Last active
June 4, 2023 13:52
-
-
Save jamesbrobb/f4c680f0d7690fe190f37058ade93612 to your computer and use it in GitHub Desktop.
Typescript generics cheatsheet
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
/* | |
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