阿里云主机折上折
  • 微信号
Current Site:Index > The `infer` keyword and type extraction

The `infer` keyword and type extraction

Author:Chuan Chen 阅读数:41374人阅读 分类: TypeScript

Basic Concept of the infer Keyword

The infer keyword in TypeScript is used to declare a type variable to be inferred within conditional types. It allows us to perform pattern matching in the type system, extracting the parts we care about from complex types. infer is typically used with conditional types, especially when working with function types, array types, and Promise types.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

This simple example demonstrates the basic usage of infer: we declare a type variable R, which represents the return type of a function type. If T is a function type, it returns R; otherwise, it returns never.

How infer Works

The infer keyword operates similarly to pattern matching in the type system. When the TypeScript compiler encounters infer, it attempts to "deconstruct" the matching part from the given type. This process can be understood as:

  1. Checking whether the type matches the pattern in the conditional type.
  2. If it matches, binding the type variable declared by infer to the corresponding part.
  3. Using this inferred type in the true branch of the conditional type.
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;

type Num = UnpackPromise<Promise<number>>;  // number
type Str = UnpackPromise<string>;          // string

In this example, the UnpackPromise type checks whether T is a Promise type. If it is, it extracts the wrapped type U; if not, it returns T itself.

Common Use Cases

Extracting Function Return Types

ReturnType is a built-in utility type in TypeScript that uses infer to extract a function's return type:

function getUser() {
  return { name: 'Alice', age: 30 };
}

type User = ReturnType<typeof getUser>;
// { name: string; age: number; }

Extracting Function Parameter Types

Similarly, we can extract the types of function parameters:

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function greet(name: string, age: number) {
  return `Hello, ${name}! You are ${age} years old.`;
}

type GreetParams = Parameters<typeof greet>;  // [string, number]

Extracting Array Element Types

infer is also very useful when working with array types:

type ElementType<T> = T extends (infer U)[] ? U : T;

type Num = ElementType<number[]>;      // number
type Str = ElementType<string[]>;      // string
type Bool = ElementType<boolean>;      // boolean

Extracting Promise Resolution Types

When working with asynchronous code, it's often necessary to extract the type resolved by a Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T;

type Num = Awaited<Promise<number>>;  // number
type Str = Awaited<string>;           // string

Advanced Usage

Recursive Type Extraction

infer can be used to build recursive types for handling nested structures:

type DeepUnpackPromise<T> = 
  T extends Promise<infer U> 
    ? DeepUnpackPromise<U> 
    : T;

type Num = DeepUnpackPromise<Promise<Promise<number>>>;  // number

infer in Union Types

Using infer in union types can extract common parts:

type CommonReturn<T> = 
  T extends (...args: any[]) => infer R ? R : never;

type F1 = () => string;
type F2 = () => number;

type R = CommonReturn<F1 | F2>;  // string | number

Tuple Type Manipulation

infer can be used to manipulate tuple types:

type First<T extends any[]> = T extends [infer U, ...any[]] ? U : never;
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never;

type F = First<[string, number, boolean]>;  // string
type R = Rest<[string, number, boolean]>;   // [number, boolean]

Practical Application Examples

Implementing a Simple State Machine Type

type StateTransitions = {
  idle: { type: 'start'; payload: string };
  running: { type: 'pause' } | { type: 'stop'; payload: number };
  paused: { type: 'resume' } | { type: 'stop'; payload: number };
};

type EventType<T> = T extends { type: infer U } ? U : never;
type EventPayload<T, K> = T extends { type: K; payload: infer P } ? P : never;

type IdleEvent = EventType<StateTransitions['idle']>;  // 'start'
type StartPayload = EventPayload<StateTransitions['idle'], 'start'>;  // string

Building Reactive Form Types

type FormField<T> = {
  value: T;
  isValid: boolean;
  error?: string;
};

type ExtractFormValue<T> = T extends FormField<infer U> ? U : never;

type UsernameField = FormField<string>;
type PasswordField = FormField<string>;

type Username = ExtractFormValue<UsernameField>;  // string
type Password = ExtractFormValue<PasswordField>;  // string

Combining with Other Advanced Types

Combining with Mapped Types

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

class User {
  name: string;
  age: number;
  greet() { return `Hello`; }
}

type UserMethods = FunctionProperties<User>;  // { greet: () => string; }

Combining with Conditional Type Distribution

type Box<T> = { value: T };
type Unbox<T> = T extends Box<infer U> ? U : T;

type A = Unbox<Box<number>>;      // number
type B = Unbox<string>;           // string
type C = Unbox<Box<string> | Box<number>>;  // string | number

Notes and Limitations

Position Restrictions for infer

infer declarations can only appear in covariant positions (such as function return types or Promise resolution types) within the extends clause of a conditional type, not in contravariant positions (such as function parameter types):

// Incorrect usage - `infer` in a contravariant position
type ParamType<T> = T extends (infer P) => any ? P : never;

// Correct usage
type ParamType<T> = T extends (...args: infer P) => any ? P : never;

Using Multiple infer Declarations

You can use multiple infer declarations in a single conditional type:

type FlipArgs<T> = 
  T extends (...args: infer A) => infer R 
    ? (...args: Reverse<A>) => R 
    : never;

// Assuming `Reverse` is a type that can reverse a tuple
type Reversed = FlipArgs<(a: string, b: number) => boolean>;
// Type becomes (b: number, a: string) => boolean

Depth Limitations for Type Inference

TypeScript has some limitations on deeply nested type inference. Overly complex types may cause performance issues or inference failures:

// This type may encounter issues with deep nesting
type DeepInfer<T> = 
  T extends Promise<infer U> 
    ? DeepInfer<U> 
    : T extends Array<infer V> 
      ? DeepInfer<V>[] 
      : T;

Practical Tips for Type Extraction

Extracting Constructor Types

type ConstructorParameters<T> = 
  T extends new (...args: infer P) => any ? P : never;

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonParams = ConstructorParameters<typeof Person>;  // [string, number]

Extracting Instance Types

type InstanceType<T> = 
  T extends new (...args: any[]) => infer R ? R : any;

type PersonInstance = InstanceType<typeof Person>;  // Person

Handling Overloaded Functions

For overloaded functions, infer will match the last overload signature:

function overloaded(a: string): number;
function overloaded(a: number): string;
function overloaded(a: any): any {
  return typeof a === 'string' ? a.length : a.toString();
}

type OverloadedReturn = ReturnType<typeof overloaded>;  // string

Applications of Type Extraction in Library Development

Implementing a Simple DI Container Type

interface ServiceContainer {
  get<T>(key: string): T;
}

type ServiceKeys<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : never;
};

function createContainer<T>(services: T): ServiceContainer & ServiceKeys<T> {
  // Implementation omitted
  return {} as any;
}

const container = createContainer({
  userService: () => ({ getUser: (id: number) => ({ id, name: 'Alice' }) }),
  logger: () => ({ log: (msg: string) => console.log(msg) })
});

type UserService = typeof container['userService'];  // { getUser: (id: number) => { id: number; name: string; } }
type Logger = typeof container['logger'];            // { log: (msg: string) => void }

Building a Type-Safe Redux Reducer

type Action<T extends string = string, P = any> = {
  type: T;
  payload?: P;
};

type ActionCreator<T extends string, P> = (payload?: P) => Action<T, P>;

type ExtractActionType<AC> = AC extends ActionCreator<infer T, any> ? T : never;
type ExtractActionPayload<AC, T> = AC extends ActionCreator<T, infer P> ? P : never;

function createAction<T extends string, P>(type: T): ActionCreator<T, P> {
  return (payload?: P) => ({ type, payload });
}

const increment = createAction('INCREMENT');
const setValue = createAction('SET_VALUE', (value: string) => value);

type IncrementType = ExtractActionType<typeof increment>;        // 'INCREMENT'
type SetValuePayload = ExtractActionPayload<typeof setValue, 'SET_VALUE'>;  // string

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:条件类型应用

下一篇:递归类型定义

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.