Skip to content

Instantly share code, notes, and snippets.

@davidmurdoch
Last active September 29, 2021 07:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidmurdoch/3145903f3a267543568225c68dcc325b to your computer and use it in GitHub Desktop.
Save davidmurdoch/3145903f3a267543568225c68dcc325b to your computer and use it in GitHub Desktop.
Typesafe, `0x` prefixed, hex strings up to 32 bytes long
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;
@davidmurdoch
Copy link
Author

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 of 0x123.

@gnidan
Copy link

gnidan commented Sep 11, 2020

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;

@davidmurdoch
Copy link
Author

Updated with @gnidan's improvements!

@gnidan
Copy link

gnidan commented Sep 11, 2020

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;

@davidmurdoch
Copy link
Author

Updated again! Thanks, @gnidan!!!

@davidmurdoch
Copy link
Author

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