HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

35 Extracting parts of compound types via infer

In this chapter, we explore how we can extract parts of compound types via the infer keyword.

It helps if you are loosely familiar with conditional types.

35.1 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.

35.1.1 Example: extracting property keys and values via 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 PropValues that extracts property values:

type PropValues<T> = T extends Record<any, infer V> ? V : never;
type _2 = Assert<Equal<
  PropValues<typeof Color>,
  0 | 1 | 2
>>;

35.2 Constraining infer via extends

In some cases, we need to help infer by using extends to constrain what can be inferred – e.g. (based on an idea by Heribert Schütz):

type EnumFromTuple<T extends ReadonlyArray<string>> =
  T extends [
    infer First extends string, // (A)
    ...infer Rest extends ReadonlyArray<string> // (B)
  ]
    ? Record<First, First> & EnumFromTuple<Rest>
    : {}
  ;
type _ = [
  Assert<Equal< // (C)
    EnumFromTuple<['a', 'b']>,
    Record<'a', 'a'> & Record<'b', 'b'>
  >>,
  Assert<Equal< // (D)
    EnumFromTuple<['a', 'b']>,
    { a: 'a', b: 'b' }
  >>,
];

Notes:

35.3 Built-in utility types that use infer

TypeScript has several built-in utility types. In this section, we look at those that use infer.

35.3.1 Extracting parts of function types via 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
>>;

35.3.2 Extracting parts of class types via infer

The following non-built-in 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:

const pointAsValue: Class<Point> = Point;
type _ = [
  Assert<Equal<
    ConstructorParameters<typeof Point>,
    [x: number, y: number]
  >>,
  Assert<Equal<
    InstanceType<typeof Point>,
    Point
  >>,
];

35.4 Example: synchronous version of an asynchronous interface

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> // (A)
    ? (...args: A) => R // (B)
    : Intf[K] // (C)
};

Let’s apply Syncify to an interface AsyncService with Promise-based methods:

interface AsyncService {
  factorize(num: number): Promise<Array<number>>;
  createDigest(text: string): Promise<string>;
}
type SyncService = Syncify<AsyncService>;
type _ = Assert<Equal<
  SyncService,
  {
    factorize: (num: number) => Array<number>,
    createDigest: (text: string) => string,
  }
>>;

35.5 Using infer to define local type variables

We can use infer to define local type variables such as W below:

type WrapTriple<T> = Promise<T> extends infer W
  ? [W, W, W]
  : never
;
type _ = Assert<Equal<
  WrapTriple<number>,
  [Promise<number>, Promise<number>, Promise<number>]
>>;

For more information, see “Defining local type variables” (§33.8).

35.6 Sources of this chapter