One of the interesting aspects in Typescript is that it is easy to extract out individual types from composite types.
In case of Array, Tuple and Object types, this is really simple, as the type extraction syntax mimic member access syntax.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type Person = { name: string; age: number; } type PersonName = Person["name"]; // string type StrNumTuple = [string, number]; type StrNumTuple0 = StrNumTuple[0]; // string type NumArr = number[]; type NumArrMember = NumArr[0]; // number |
The object access syntax works for interfaces too, which is quite intuitive:
1 2 3 4 5 6 |
interface Person { name: string; age: number; } type PersonName = Person["name"]; // string |
However, more interesting is that, we can extract types from generics and functions too.
Let us say we have the following Dictionary type
1 2 3 4 5 |
interface Dictionary<T = any> { [key: string]: T; } type StrDict = Dictionary<string> |
To extract T
from StrDict
, we can use the above member property approach:
1 |
type StrDictMember = StrDict[""]; // string |
However, another alternative here is to use typescript’s infer
keyword along with conditional types:
1 2 |
type DictMember<T> = T extends Dictionary<infer V> ? V : never type StrDictMember = DictMember<StrDict> |
The conditional type introduced in Typescript 2.8 allows us to arrive at different types based on some conditions. The condition here being a type compatibility constraint.
Not that even though extends
keyword is used, this does not necessarily enforce an inheritance relationship, but rather checks for structural compatibility.
infer
introduces a type variable here, which can be later collected in the type the conditional type resolves to.
This can be used to collect types from arbitrary generic types using typescript’s inference algorithm.
The same appraoch can be used to extract types from functions too. For example, following type (borrowed from typescript’s collection of built in types), allows us to extract the return value of function.
1 |
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any; |
The analogous approach can be used for arguments too:
1 2 3 4 |
type Fn1 = (a: number) => string; type ArgType<T> = T extends ((a: (infer U)) => any) ? U : never; type Fn1Arg = ArgType<Fn1>; // number |
Things get somewhat more interesting, if we want to extract the tuple type of all the arguments.
Typescript 3.0-rc introduces support for extracting and spreading parameter lists with tuples. So using typescript@next
we can do something like:
1 2 3 4 5 |
type VariadicFn<A extends any[]> = (...args: A) => any; type ArgsType<T> = T extends VariadicFn<infer A> ? A : never; type Fn2 = (a: number, b: string) => string; type Fn2Args = ArgsType<Fn2>; // [number, string] |
It may be tempting to do something like:
1 |
type ArgsType<T> = T extends (...args: (infer U)[]) => any ? U[] : never; |
This will not work as expected, because U
now has to satisfy the type at every position.
1 2 |
type Fn2 = (a: number, b: string) => string; type Fn2Args = ArgsType<Fn2>; // (number & string)[] |