infer
In this blog post, we explore how we can extract parts of composite types via the infer
operator.
It helps if you are loosely familiar with conditional types. You can check out section “Conditional types” in “Tackling TypeScript” to read up on them.
For showing computed and inferred types in source code, I use the npm package asserttt
– e.g.:
// Types of values
assertType<string>('abc');
assertType<number>(123);
// Equality of types
type Pair<T> = [T, T];
type _ = Assert<Equal<
Pair<string>, [string,string]
>>;
infer
: extracting types inside the extends
clause of a conditional type infer
is used inside the constraint part of a conditional type:
type _ = Type extends Constraint ? <then> : <else>;
A constraint can be a type pattern – e.g.:
T[]
Promise<T>
(arg: T) => R
At any location where we can mention an existing type variable (such as T
or R
in the previous examples), we can also introduce a new type variable X
via infer X
. Let’s look at an example: The following generic type ElemType<Arr>
extracts the element type of an Array type:
type ElemType<Arr> = Arr extends Array<infer Elem> ? Elem : never;
type _ = Assert<Equal<
ElemType<Array<string>>, string
>>;
infer
has a lot in common with destructuring in JavaScript.
Record
The utility type Record
lets us implement the keyof
operator ourselves:
const Color = {
red: 0,
green: 1,
blue: 2,
} as const;
type KeyOf<T> = T extends Record<infer K, any> ? K : never;
type _1 = Assert<Equal<
KeyOf<typeof Color>,
"red" | "green" | "blue"
>>;
We can also implement a utility type ValueOf
that extracts property values:
type ValueOf<T> = T extends Record<any, infer V> ? V : never;
type _2 = Assert<Equal<
ValueOf<typeof Color>,
0 | 1 | 2
>>;
infer
TypeScript has several built-in utility types. In this section, we look at those that use infer
.
infer
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
Both of these utility types are straightforward: We put infer
where we want to extract a type. Let’s use both types:
function add(x: number, y: number): number {
return x + y;
}
type _1 = Assert<Equal<
typeof add,
(x: number, y: number) => number
>>;
type _2 = Assert<Equal<
Parameters<typeof add>,
[x: number, y: number]
>>;
type _3 = Assert<Equal<
ReturnType<typeof add>,
number
>>;
infer
The following utility type for classes demonstrate how construct signatures work:
type Class<T> = abstract new (...args: Array<any>) => T;
This type includes all classes whose instances have the type T
. The keyword abstract
means that it includes both abstract and concrete classes. Without that keyword, it would only include classes that are instantiable.
Construct signatures enable us to extract parts of classes:
/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
To demonstrate these utility types, let’s define a class we can apply them to:
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
Note that the class Point
defines two things:
Point
for instances of the class.Point
. That value has the type Class<Point>
.const pointAsValue: Class<Point> = Point;
type _ = [
Assert<Equal<
ConstructorParameters<typeof Point>,
[x: number, y: number]
>>,
Assert<Equal<
InstanceType<typeof Point>,
Point
>>,
];
The following example is a little more complex: It converts all asynchronous methods in an object type to synchronous methods:
type Syncify<Intf> = {
[K in keyof Intf]:
Intf[K] extends (...args: infer A) => Promise<infer R>
? (...args: A) => R
: Intf[K];
};
Let’s apply Syncify
to an interface AsyncInterface
with Promise-based methods:
interface AsyncInterface {
compute(num: number): Promise<boolean>;
createString(): Promise<String>;
}
type SyncInterface = Syncify<AsyncInterface>;
type _ = Assert<Equal<
SyncInterface,
{
compute: (num: number) => boolean,
createString: () => String,
}
>>;
infer
to create aliases for complicated values With complicated types, type-level TypeScript becomes a programming language with functions, variables, conditions, loops, etc. One construct is still missing though: variable declarations. These are useful whenever we’d like to create an alias for a complicated value. However, we can emulate them via infer
:
// What a hypothetical variable declaration might look like
type Result = let Var := Value in <body>;
// Emulation via `infer`
type Result = Value extends infer Var ? <body> : never;
This is an example where this technique is useful:
type WrapTriple<T> = Promise<T> extends infer W
? [W, W, W]
: never
;
type _ = Assert<Equal<
WrapTriple<number>,
[Promise<number>, Promise<number>, Promise<number>]
>>;