Skip to content

Instantly share code, notes, and snippets.

@jamesbrobb
Last active June 4, 2023 13:52
Show Gist options
  • Save jamesbrobb/f4c680f0d7690fe190f37058ade93612 to your computer and use it in GitHub Desktop.
Save jamesbrobb/f4c680f0d7690fe190f37058ade93612 to your computer and use it in GitHub Desktop.
Typescript generics cheatsheet
/*
Array/Tuple access
type T = ['Item One', 'Item Two', 'Item Three', 'Item Four']
type Util<T extends readonly unknown[]> = % example %
*/
T['length'] // 4
T[0] // 'Item One'
T['0'] // 'Item One'
// look up loop for every number key in tuple - so all entry values
T[number] // 'Item One' | 'Item Two' | 'Item Three' | 'Item Four'
// look up loop for every key in tuple - so all numeric keys and every built in key
T[keyof T] // Returns the value of every key in the Tuple... this includes all built in keys - length, concat, etc
T extends Array<infer N> ? N : never // 'Item One' | 'Item Two' | 'Item Three' | 'Item Four'
T extends (infer N)[] ? N : never // 'Item One' | 'Item Two' | 'Item Three' | 'Item Four'
T extends unknown[] ? T[number] : never // 'Item One' | 'Item Two' | 'Item Three' | 'Item Four'
T extends any[] ? T[number] : never // 'Item One' | 'Item Two' | 'Item Three' | 'Item Four'
T extends any[number] ? T[number] : never // 'Item One' | 'Item Two' | 'Item Three' | 'Item Four'
T extends [infer First, ...infer Rest] ? First : never // 'Item One'
T extends [infer First, ...infer Rest] ? Rest : never // ['Item Two', 'Item Three', 'Item Four']
T extends [...infer Start, infer Last] ? Last : never // 'Item Four'
T extends [...infer Start, infer Last] ? Start : never // ['Item One', 'Item Two', 'Item Three']
T extends Array<infer K> ? {types: K} : never // {types: "Item One" | "Item Two" | "Item Three" | "Item Four"}
[...T] // ['Item One', 'Item Two', 'Item Three', 'Item Four']
[...T, ...T] // ["Item One", "Item Two", "Item Three", "Item Four", "Item One", "Item Two", "Item Three", "Item Four"]
/*
{[K in keyof T]: MyValue / UtilityFunc }
Iterates through array/tuple replacing each entry with MyValue
type T = ['Item One', 'Item Two', 'Item Three', 'Item Four']
type Util<T extends readonly unknown[]> = % example %
*/
{[K in keyof T]: T[K]} // ['Item One', 'Item Two', 'Item Three', 'Item Four']
{[K in keyof T]: K} // ['0', '1', '2', '3']
{[K in keyof T]: T[number]} // ['Item One' | 'Item Two' | 'Item Three' | 'Item Four', ...+3]
{[K in keyof T]: T[0]} // ['Item One', 'Item One', 'Item One', 'Item One']
{[K in keyof T]: T[K] extends unknown ? (arg: K) => T[K] : never } // [(arg: "0") => "Item One", (arg: "1") => "Item Two", (arg: "2") => "Item Three", (arg: "3") => "Item Four"]
{[K in keyof T]: T[number]} extends (infer A)[] ? A : never // "Item One" | "Item Two" | "Item Three" | "Item Four"
({[K in keyof T]: K} extends (infer V)[] ? V : never) & keyof T // "0" | "1" | "2" | "3"
/*
* Creates ['0', '1', '2', '3'] then loops through using each value as a key
* to access each type in the original T Array.
*
* T[J & keyof T] - J on its own - so T[J] - is considered to be a type not an index type
*/
{[K in keyof T]: K} extends Array<infer J> ? T[J & keyof T] : never // "Item One" | "Item Two" | "Item Three" | "Item Four"
{[K in T[number]]: K} // { "Item One": "Item One", "Item Two": "Item Two", "Item Three": "Item Three", "Item Four": "Item Four" }
{[K in Exclude<keyof T, keyof []>]: T[K]} // { "0": "Item One", "1": "Item Two", "2": "Item Three", "3": "Item Four" }
/*
* ({[K in keyof T]: K} extends (infer V)[] ? V : never) & keyof T
*
* Creates an Array of the Array indexes - {[K in keyof T]: K} = [0, 1, 2, 3]
* Converts them to a union - extends (infer V)[] ? V : never = 0 | 1 | 2 | 3
* And informs the interpreter that the result is the correct type to index the original array - & keyof T
*
* Lets call the resulting Union V (0 | 1 | 2 | 3) for brevity
*
* {[K in V]: T[K]}
*
* So for each item in the union V, assign the item to K and use its value as the key
* and then use the value of K to index the original Array T to assign the value
*/
{[K in ({[K in keyof T]: K} extends (infer V)[] ? V : never) & keyof T]: T[K]} // { "0": "Item One", "1": "Item Two", "2": "Item Three", "3": "Item Four" }
/*
* Tip to remember - multiple extends in a chain may not create the desired result - so don't try and break down the problem into a linear solution
*
* My original attempt at solving the above problem was the following
*
* {[K in keyof T]: K} extends (infer V)[] ?
* V extends keyof T ?
* {[K in V]: T[K & keyof T]} :
* never :
* never
*
* So i broke it down in to what appeared (to me at least) to be logical steps:
*
* 1) Get the indexes of the Array and convert to a union
* 2) 'Cast' the resulting union to keyof T so that it can be used to index T
* 3) Create the object of numerical index keys and Array values
*
* But the result is incorrect. Instead of a single type containing all keys we end
* up with a union of types each containing a single key value pair
*
* type Result = {
* 0: "Item One";
* } | {
* 1: "Item Two";
* } | {
* 2: "Item Three";
* } | {
* 3: "Item Four";
* }
*
* The reason for this is that chaining multiple extends in this manner creates multiple loops.
* So when we get to step 3) we have the outer loop of `V extends keyof T` that's looping through the union V
* 4 times to create the resulting union of 4 types instead of 1.
*
* But without the outer loop of `V extends keyof T` the interpreter will complain as there's no guarantee that the
* resulting union type V can actually be used to index T.
*
* The simple solution to this is to move that guarantee closer to the point at which it's required.
* So we combine step 2) and step 3) to get the desired result
*
* {[K in keyof T]: K} extends (infer V)[] ?
* {[K in V extends keyof T ? V : never]: T[K]} :
* never
*
* Although this now outputs the desired result we can apply the same logic again and combine the first and second steps.
*
* {[K in ({[K in keyof T]: K} extends (infer V)[] ? V : never) & keyof T]: T[K]}
*/
/*
* Unions
*/
/*
* From @link https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types
*
* "Multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred"
*
* Essentially creates and matching function type for each type in the union, which then get interpreted as function overloads
*/
type _UnionToIntersectionHelper<U> =
(U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
// @link https://stackoverflow.com/a/67609110/1798234
type UnionToIntersection<U> = boolean extends U
? _UnionToIntersectionHelper<Exclude<U, boolean>> & boolean
: _UnionToIntersectionHelper<U>;
/*
* UnionPop
*
* U extends any ? (f: U) => void : never - wraps the type in a function to prevent eager reduction to never of impossible type intersections
*
* i.e number | boolean | string would become number & boolean & string which is eagerly reduced to never
*
* UnionToIntersection - converts the union of functions into an intersection - see explanation above utility type
*
* Finally...
*
* extends (f: infer I) => void ? I : never - infers the type from the initially created function wrapper
*
* However as the function types are now an intersection (instead of a union) and all have the same call signature, they are
* treated as function overload declarations, causing only the final function types argument type to be infered and returned
*
* @link https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types
*
* "When inferring from a type with multiple call signatures (such as the type of an overloaded function),
* inferences are made from the last signature (which, presumably, is the most permissive catch-all case)."
*
*/
type UnionPop<U> = UnionToIntersection<U extends any ? (f: U) => void : never> extends (f: infer I) => void ? I : never
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment