Last active
September 29, 2021 07:04
-
-
Save davidmurdoch/3145903f3a267543568225c68dcc325b to your computer and use it in GitHub Desktop.
Typesafe, `0x` prefixed, hex strings up to 32 bytes long
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Cast<T, U> = T extends U ? T : U; | |
type Prop<T, K> = K extends keyof T ? T[K] : never; | |
type HexChars = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f"; | |
type HexPairs = `${HexChars}${HexChars}`; | |
/** | |
* returns a Tuple of hex pairs (bytes), or `never` if the input doesn't contain an even number of hex characters | |
*/ | |
type Hex<S extends string> = | |
S extends "" ? [] : | |
S extends `${HexPairs}${infer R}` ? S extends `${infer C}${R}` | |
? [C, ...(Hex<R> extends infer X ? Cast<X, string[]> : never)] | |
: never : never; | |
/** | |
* Gets up to the first 8 bytes following the given prefix. `S` _must not_ start with `0x`. | |
*/ | |
type Raw<S extends string, P extends string = ""> = | |
S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}${infer I4}${infer I5}${infer I6}${infer I7}${infer I8}${infer I9}${infer I10}${infer I11}${infer I12}${infer I13}${infer I14}${infer I15}${infer _}` ? `${I0}${I1}${I2}${I3}${I4}${I5}${I6}${I7}${I8}${I9}${I10}${I11}${I12}${I13}${I14}${I15}` | |
: S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}${infer I4}${infer I5}${infer I6}${infer I7}${infer I8}${infer I9}${infer I10}${infer I11}${infer I12}${infer I13}` ? `${I0}${I1}${I2}${I3}${I4}${I5}${I6}${I7}${I8}${I9}${I10}${I11}${I12}${I13}` | |
: S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}${infer I4}${infer I5}${infer I6}${infer I7}${infer I8}${infer I9}${infer I10}${infer I11}` ? `${I0}${I1}${I2}${I3}${I4}${I5}${I6}${I7}${I8}${I9}${I10}${I11}` | |
: S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}${infer I4}${infer I5}${infer I6}${infer I7}${infer I8}${infer I9}` ? `${I0}${I1}${I2}${I3}${I4}${I5}${I6}${I7}${I8}${I9}` | |
: S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}${infer I4}${infer I5}${infer I6}${infer I7}` ? `${I0}${I1}${I2}${I3}${I4}${I5}${I6}${I7}` | |
: S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}${infer I4}${infer I5}` ? `${I0}${I1}${I2}${I3}${I4}${I5}` | |
: S extends `${P}${infer I0}${infer I1}${infer I2}${infer I3}` ? `${I0}${I1}${I2}${I3}` | |
: S extends `${P}${infer I0}${infer I1}` ? `${I0}${I1}` | |
: ""; | |
/** | |
* Add up all our lengths. If any length is `never`, this returns `never`. | |
*/ | |
type ValidLengthTogether<L1 extends number, L2 extends number, L3 extends number, L4 extends number> = ( | |
L1 extends 8 | |
? L2 extends 8 | |
? L3 extends 8 | |
? Add<`${L4}`, "24"> | |
: Add<`${L3}`, "16"> | |
: Add<`${L2}`, "8"> | |
: L1 extends never | |
? never | |
: `${L1}` | |
); | |
type Trim<S extends string, Prefix extends string> = S extends `${Prefix}${infer I}` ? I : never; | |
type U1<S extends string> = Raw<S>; | |
type U2<S extends string> = Raw<S, `${U1<S>}`>; | |
type U3<S extends string> = Raw<S, `${U1<S>}${U2<S>}`>; | |
type U4<S extends string> = Raw<S, `${U1<S>}${U2<S>}${U3<S>}`>; | |
type L1<S extends string> = Prop<Hex<U1<S>>, "length">; | |
type L2<S extends string> = Prop<Hex<U2<S>>, "length">; | |
type L3<S extends string> = Prop<Hex<U3<S>>, "length">; | |
type L4<S extends string> = Prop<Hex<U4<S>>, "length">; | |
// @ts-ignore: Ignore type instantiation depth errors as we have already limited our depth | |
type ValidLength<S extends string> = ValidLengthTogether<L1<S>, L2<S>, L3<S>, L4<S>>; | |
type Trimmed<S extends string> = Trim<S, "0x">; | |
/** | |
* Supports hex-encoded strings up to 32 bytes (64 hex chars), `0x` prefixed | |
*/ | |
type DATA< | |
S extends string, | |
Length extends number = -1, | |
> = Length extends -1 | |
? ValidLength<Trimmed<S>> extends never | |
? never | |
: S | |
: `${Length}` extends ValidLength<Trimmed<S>> | |
? S | |
: never; | |
/* ************************** <HELPER TYPES> ********************************** */ | |
type TxHash<S extends string> = DATA<S, 32>; | |
type Address<S extends string> = DATA<S, 20>; | |
type Nonce<S extends string> = DATA<S, 4>; | |
/* ************************** </HELPER TYPES> ********************************* */ | |
/* ************************** <DEFINE OPTIONS> ******************************** */ | |
type BaseOptions = { | |
nonce: string; | |
coinbase: string; | |
}; | |
type OptionsTypeMap<N> = { | |
nonce: Nonce<Cast<N, string>>, | |
coinbase: Address<Cast<N, string>> | |
} | |
type Cond<B, T extends B, U> = B extends T ? B : U; | |
type ValidOptions<O extends BaseOptions> = { | |
[P in keyof O]?: | |
P extends keyof OptionsTypeMap<O[P]> | |
? Cond<string, O[P], OptionsTypeMap<O[P]>[P]> | |
: O[P] | |
}; | |
/* ************************** </DEFINE OPTIONS> ******************************* */ | |
/* ************************** <BASIC TESTS> *********************************** */ | |
type a = DATA<"0x123"> // never | |
type b = DATA<"0x123", 3> // never (odd numbers aren't currently allowed) | |
type c = DATA<"0x1234"> // 0x1234 | |
type d = DATA<"0xbda2ca0611c2a952a4a25bfc175803912842c839", 4> // never, too long | |
type e = DATA<"0xbda2ca0611c2a952a4a25bfc175803912842c839"> // 0xbda2ca0611c2a952a4a25bfc175803912842c839 | |
type f = DATA<"0xbda2ca0611c2a952a4a25bfc175803912842c839", 20> // 0xbda2ca0611c2a952a4a25bfc175803912842c839 | |
type g = DATA<"0xff6b938acb7d72e261c51fa255a8119d9e23d021bed440298f91342706b1538d"> // 0xff6b938acb7d72e261c51fa255a8119d9e23d021bed440298f91342706b1538d | |
type h = DATA<"0xff6b938acb7d72e261c51fa255a8119d9e23d021bed440298f91342706b1538d", 32> // 0xff6b938acb7d72e261c51fa255a8119d9e23d021bed440298f91342706b1538d | |
type i = DATA<"0123"> // never, invalid | |
/* ************************** </BASIC TESTS> *********************************** */ | |
/* ************************** <COMPLEX TESTS> ********************************** */ | |
declare function provider< | |
O extends BaseOptions | |
>(options: ValidOptions<O>): void; | |
// VALID INPUT: | |
provider({ | |
coinbase: "0xbda2ca0611c2a952a4a25bfc175803912842c839", | |
nonce: "0xa3079c9f" | |
}); | |
provider({ | |
coinbase: "0xbda2ca0611c2a952a4a25bfc175803912842c839" | |
}); | |
provider({ | |
nonce: "0xa3079c9f" | |
}); | |
// INVALID INPUTS: | |
provider({ | |
coinbase: "0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", // invalid chars | |
nonce: "0xa3079c9f" | |
}); | |
provider({ | |
coinbase: "0x1234", // too short | |
nonce: "0xa3079c9f" | |
}); | |
provider({ | |
coinbase: "0x", // too short | |
nonce: "0xa3079c9f" | |
}); | |
provider({ | |
coinbase: "0", // no 0x and too short | |
nonce: "0xa3079c9f" | |
}); | |
provider({ | |
coinbase: "bda2ca0611c2a952a4a25bfc175803912842c839", // no 0x | |
nonce: "0xa3079c9f" | |
}); | |
provider({ | |
coinbase: "0xbda2ca0611c2a952a4a25bfc175803912842c839", | |
nonce: "a3079c9f" // no 0x | |
}); | |
provider({ | |
coinbase: "0xbda2ca0611c2a952a4a25bfc175803912842c839", | |
nonce: "0xQ3079c9f" // invalid character (Q) | |
}); | |
provider({ | |
coinbase: "0xbda2ca0611c2a952a4a25bfc175803912842c839", | |
nonce: "0xA3079c9f123456789" // too long | |
}); | |
/* ************************** </COMPLEX TESTS> ********************************* */ | |
/* ***************************************************************************** */ | |
/* ***************************************************************************** */ | |
/* ***************************************************************************** */ | |
/* ***************************************************************************** */ | |
/* ***************************** STRING ADDITION ******************************* */ | |
/* ***************************************************************************** */ | |
/* ***************************************************************************** */ | |
/* ***************************************************************************** */ | |
/* ***************************************************************************** */ | |
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; | |
type OneDigitPlus = { | |
"00": [false, "0"]; | |
"01": [false, "1"]; | |
"02": [false, "2"]; | |
"03": [false, "3"]; | |
"04": [false, "4"]; | |
"05": [false, "5"]; | |
"06": [false, "6"]; | |
"07": [false, "7"]; | |
"08": [false, "8"]; | |
"09": [false, "9"]; | |
"10": [false, "1"]; | |
"11": [false, "2"]; | |
"12": [false, "3"]; | |
"13": [false, "4"]; | |
"14": [false, "5"]; | |
"15": [false, "6"]; | |
"16": [false, "7"]; | |
"17": [false, "8"]; | |
"18": [false, "9"]; | |
"19": [true, "0"]; | |
"20": [false, "2"]; | |
"21": [false, "3"]; | |
"22": [false, "4"]; | |
"23": [false, "5"]; | |
"24": [false, "6"]; | |
"25": [false, "7"]; | |
"26": [false, "8"]; | |
"27": [false, "9"]; | |
"28": [true, "0"]; | |
"29": [true, "1"]; | |
"30": [false, "3"]; | |
"31": [false, "4"]; | |
"32": [false, "5"]; | |
"33": [false, "6"]; | |
"34": [false, "7"]; | |
"35": [false, "8"]; | |
"36": [false, "9"]; | |
"37": [true, "0"]; | |
"38": [true, "1"]; | |
"39": [true, "2"]; | |
"40": [false, "4"]; | |
"41": [false, "5"]; | |
"42": [false, "6"]; | |
"43": [false, "7"]; | |
"44": [false, "8"]; | |
"45": [false, "9"]; | |
"46": [true, "0"]; | |
"47": [true, "1"]; | |
"48": [true, "2"]; | |
"49": [true, "3"]; | |
"50": [false, "5"]; | |
"51": [false, "6"]; | |
"52": [false, "7"]; | |
"53": [false, "8"]; | |
"54": [false, "9"]; | |
"55": [true, "0"]; | |
"56": [true, "1"]; | |
"57": [true, "2"]; | |
"58": [true, "3"]; | |
"59": [true, "4"]; | |
"60": [false, "6"]; | |
"61": [false, "7"]; | |
"62": [false, "8"]; | |
"63": [false, "9"]; | |
"64": [true, "0"]; | |
"65": [true, "1"]; | |
"66": [true, "2"]; | |
"67": [true, "3"]; | |
"68": [true, "4"]; | |
"69": [true, "5"]; | |
"70": [false, "7"]; | |
"71": [false, "8"]; | |
"72": [false, "9"]; | |
"73": [true, "0"]; | |
"74": [true, "1"]; | |
"75": [true, "2"]; | |
"76": [true, "3"]; | |
"77": [true, "4"]; | |
"78": [true, "5"]; | |
"79": [true, "6"]; | |
"80": [false, "8"]; | |
"81": [false, "9"]; | |
"82": [true, "0"]; | |
"83": [true, "1"]; | |
"84": [true, "2"]; | |
"85": [true, "3"]; | |
"86": [true, "4"]; | |
"87": [true, "5"]; | |
"88": [true, "6"]; | |
"89": [true, "7"]; | |
"90": [false, "9"]; | |
"91": [true, "0"]; | |
"92": [true, "1"]; | |
"93": [true, "2"]; | |
"94": [true, "3"]; | |
"95": [true, "4"]; | |
"96": [true, "5"]; | |
"97": [true, "6"]; | |
"98": [true, "7"]; | |
"99": [true, "8"]; | |
} | |
type LastDigit<Str, D = Digit> = D extends Digit ? Str extends `${infer Rest}${D}` ? [Rest, D] : never : never; | |
type ConcatStr<Left, Right> = | |
Left extends string ? | |
Right extends string ? | |
`${Left}${Right}` | |
: never : never; | |
type Add<Left, Right> = | |
Left extends "" ? Right : | |
Right extends "" ? Left : | |
LastDigit<Left> extends [infer LeftRest, infer LeftDigit] ? | |
LastDigit<Right> extends [infer RightRest, infer RightDigit] ? | |
Add2<LeftRest, RightRest, ConcatStr<LeftDigit, RightDigit>> : never : never; | |
type Add2<LeftRest, RightRest, LR> = | |
LR extends keyof OneDigitPlus ? | |
OneDigitPlus[LR] extends [infer Carry, infer LD] ? | |
Add<LeftRest, RightRest> extends infer RestSum ? | |
(Carry extends true ? | |
Add<RestSum, "1"> : | |
RestSum) extends infer LeftCarriedSum ? | |
ConcatStr<LeftCarriedSum, LD> | |
: never : never : never : never; | |
type AddD<Left, Right> = | |
Left extends "" ? Right : | |
Right extends "" ? Left : | |
LastDigit<Left> extends [infer LeftRest, infer LeftDigit] ? | |
LastDigit<Right> extends [infer RightRest, infer RightDigit] ? | |
[LeftRest, RightRest, ConcatStr<LeftDigit, RightDigit>] : never : never; |
Got rid of 8 type params on DATA
:
/**
* Add up all our lengths. If any length is `never`, this returns `never`.
*/
type ValidLengthTogether<L1 extends number, L2 extends number, L3 extends number, L4 extends number> = (
L1 extends 8
? L2 extends 8
? L3 extends 8
? Add<`${L4}`, "24">
: Add<`${L3}`, "16">
: Add<`${L2}`, "8">
: L1 extends never
? never
: `${L1}`
);
type Trim<S extends string, Prefix extends string> = S extends `${Prefix}${infer I}` ? I : never;
type U1<S extends string> = Raw<S>;
type U2<S extends string> = Raw<S, `${U1<S>}`>;
type U3<S extends string> = Raw<S, `${U1<S>}${U2<S>}`>;
type U4<S extends string> = Raw<S, `${U1<S>}${U2<S>}${U3<S>}`>;
type L1<S extends string> = Prop<Hex<U1<S>>, "length">;
type L2<S extends string> = Prop<Hex<U2<S>>, "length">;
type L3<S extends string> = Prop<Hex<U3<S>>, "length">;
type L4<S extends string> = Prop<Hex<U4<S>>, "length">;
// @ts-ignore: Ignore type instantiation depth errors as we have already limited our depth
type ValidLength<S extends string> = ValidLengthTogether<L1<S>, L2<S>, L3<S>, L4<S>>;
type Trimmed<S extends string> = Trim<S, "0x">;
/**
* Supports hex-encoded strings up to 32 bytes (64 hex chars), `0x` prefixed
*/
type DATA<
S extends string,
Length extends number = -1,
> = Length extends -1
? ValidLength<Trimmed<S>> extends never
? never
: S
: `${Length}` extends ValidLength<Trimmed<S>>
? S
: never;
Updated with @gnidan's improvements!
Reducing provider
to a single type param:
type BaseOptions = {
nonce: string;
coinbase: string;
};
type OptionsTypeMap<N> = {
nonce: Nonce<Cast<N, string>>,
coinbase: Address<Cast<N, string>>
}
type Cond<B, T extends B, U> = B extends T ? B : U;
type ValidOptions<O extends BaseOptions> = {
[P in keyof O]?:
P extends keyof OptionsTypeMap<O[P]>
? Cond<string, O[P], OptionsTypeMap<O[P]>[P]>
: O[P]
};
declare function provider<
O extends BaseOptions
>(options: ValidOptions<O>): void;
Updated again! Thanks, @gnidan!!!
For more fun things you can do with this see: https://gist.github.com/davidmurdoch/de393cca445effbed4d7e22fe33c5b85
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This utilizes template literals types that will (hopefully) be introduced in TypeScript 4.1.0 beta.
To try it today you can go to https://www.typescriptlang.org/play?#code/Q, switch the version to
Nightly
, then copy+paste this gist in!Note: this version only handles complete hex pairs, like
0x0123
instead of0x123
.