TypeScript: extracting parts of composite types via infer

[2025-02-10] dev, typescript
(Ad, please don’t block)

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.

Notation used in this blog post  

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.

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 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
>>;

Built-in utility types that use infer  

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

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
>>;

Extracting parts of class types via 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:

  • A type: the type Point for instances of the class.
  • A value: a factory for objects whose type is 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
  >>,
];

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>
    ? (...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,
  }
>>;

Using 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>]
>>;

Further reading