jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

TypeScript Production · Phần 6 — infer: Pattern Matching & Type Extraction

Làm chủ infer từ ReturnType tới candidate resolution, variadic tuple, constrained infer, recursive extraction và các contract router, i18n, state machine trong production.

22 MIN READ Updated JUL 12, 2026

Lần đầu nhìn vào dòng này, có lẽ bạn đã từng confuse:

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

infer R ở đâu ra? Vì sao TS lại “tự biết” R chính là return type của hàm? Đây không phải magic — infercơ chế bind type variable cho phép TS extract một sub-type từ vị trí bất kỳ trong type expression. Khi đã hiểu nó, hàng loạt utility tưởng chừng huyền bí (Parameters, Awaited, ConstructorParameters) trở nên hiển nhiên.

Đi xa hơn, inferxương sống của những type magic mà các thư viện như tRPC, Zod, react-hook-form, tanstack-router dùng để cho IDE gợi ý chính xác từng key, từng param, từng route — tất cả ở compile time, không có runtime cost.

Bài này dành cho TypeScript developer đã quen generic và conditional type, muốn hiểu sâu infer từ pattern cơ bản (ReturnType) tới advanced (parse string với template literal, recursive type, infer extends), và biết khi nào nên dùng vs lạm dụng.


Mục lục

  1. Câu chuyện vì sao infer xuất hiện

Part I — Foundations

  1. Recap: conditional type T extends U ? X : Y
  2. infer 101 — bind type variable trong vị trí bất kỳ
  3. 7 utility built-in dùng infer

Part II — Intermediate patterns

  1. Tuple / array manipulation: Head, Tail, Last, Reverse
  2. Function infer: composition, curry, pipe

Part III — Advanced patterns

  1. Template literal + infer — parse string ở compile time
  2. Recursive infer — DeepReadonly, Path, Get
  3. infer extends (TS 4.7+) — constrained inference

Part IV — Real-world

  1. 5 use case thực chiến
  2. Pitfalls, cheat sheet & Kết luận

1. Câu chuyện vì sao infer xuất hiện

Trước TS 2.8 (2018), nếu muốn extract return type của một hàm, bạn phải viết utility ReturnType thủ công bằng overload trick — clunky và không work với arrow function generic. TS team thêm infer để giải quyết vấn đề “tôi muốn đặt type variable trong vị trí bất kỳ và cho compiler auto-bind nó”.

Trước/sau khi có infer:

// Trước (TS < 2.8) — không thể viết kiểu này
// Phải dùng overload + signature trick rất xấu

// Sau (TS 2.8+) — declarative, đẹp
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: 'Vinh' };
}

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

Cú pháp // ^? (Twoslash) là convention show inferred type ngay vị trí con trỏ. Trong VSCode bạn sẽ thấy tooltip này khi hover. Bài này dùng nó liên tục để show kết quả TS infer.

Mental model: infer X“đặt một biến type ở đây và đợi TS điền giá trị vào”. Compiler chạy unification giữa input type và pattern, gán giá trị thực cho X.


2. Recap: conditional type T extends U ? X : Y

infer chỉ xuất hiện trong conditional type — nên cần nắm vững foundation này trước.

Cú pháp cơ bản

type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<'hello'>; // 'yes'
type B = IsString<42>; // 'no'

Distributive conditional type

Khi Tnaked type parameter (không bọc trong tuple/object) và là union, conditional type distribute qua từng phần tử của union:

type ToArray<T> = T extends any ? T[] : never;

type R1 = ToArray<string | number>;
//   ^? string[] | number[]    ← KHÔNG phải (string | number)[]

// Tắt distribution: bọc T trong tuple [T]
type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;

type R2 = ToArrayNoDist<string | number>;
//   ^? (string | number)[]

Distribution là cực kỳ quan trọng khi dùng với infer — nhiều bug xuất phát từ việc quên rằng conditional type distribute.

Conditional kết hợp

type IsArray<T> = T extends readonly any[] ? true : false;

type IsFunction<T> = T extends (...args: any) => any ? true : false;

type IsPromise<T> = T extends Promise<any> ? true : false;

3 pattern này là tiền đề cho mọi ReturnType-style helper sau.


3. infer 101 — bind type variable trong vị trí bất kỳ

infer X chỉ xuất hiện ở bên trái của conditional (sau extends). Nó bind một type variable mới chỉ visible trong nhánh true:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
//                                                   ─┬─    ─
//                                                    │     └── R có thể dùng ở đây
//                                                    └── và chỉ ở đây

type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<(n: number) => User>; // User
type R3 = ReturnType<'not a function'>; // never (fallback branch)

Multiple infer trong cùng pattern

Có thể đặt nhiều infer cùng lúc — TS bind từng cái độc lập:

type FirstAndLast<T extends readonly any[]> = T extends readonly [
  infer First,
  ...any[],
  infer Last,
]
  ? [First, Last]
  : T extends readonly [infer Only]
    ? [Only, Only]
    : never;

type A = FirstAndLast<[1, 2, 3, 4]>;
//   ^? [1, 4]

type B = FirstAndLast<['only']>;
//   ^? ['only', 'only']

infer cùng tên xuất hiện 2 lần

Khi infer X xuất hiện ở co-variant position (output, return) nhiều lần → TS lấy union. Khi ở contra-variant position (input, parameter) → lấy intersection:

// Co-variant: union
type Co<T> = T extends { a: infer U; b: infer U } ? U : never;
type CoR = Co<{ a: string; b: number }>;
//   ^? string | number

// Contra-variant: intersection
type Contra<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
}
  ? U
  : never;
type ContraR = Contra<{ a: (x: string) => void; b: (x: number) => void }>;
//   ^? string & number  → never

Đây là detail đẹp nhưng khó gặp trong code thực tế. Nhớ rule “co = union, contra = intersection” là đủ.


4. 7 utility built-in dùng infer

Hầu hết utility type “magic” trong lib.es5.d.ts đều là conditional + infer. Hiểu chúng là cách nhanh nhất để học pattern.

ReturnType<T>

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

type Fn = (a: number) => User;
type R = ReturnType<Fn>;
//   ^? User

Parameters<T>

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

type P = Parameters<(name: string, age: number) => void>;
//   ^? [name: string, age: number]   ← labeled tuple, không phải [string, number]

Lưu ý: TS giữ parameter label (name: string) trong tuple — pattern này hữu ích khi forward args với type-safe naming.

ConstructorParameters<T>

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

class Logger {
  constructor(name: string, level: 'info' | 'debug') {}
}

type Args = ConstructorParameters<typeof Logger>;
//   ^? [name: string, level: 'info' | 'debug']

InstanceType<T>

Lấy kiểu instance của một class constructor:

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

type LoggerInst = InstanceType<typeof Logger>;
//   ^? Logger

Awaited<T> (TS 4.5+) — recursive!

Unwrap Promise lồng nhau bao nhiêu cấp cũng được:

type Awaited<T> = T extends null | undefined
  ? T
  : T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
    ? F extends (value: infer V, ...args: infer _) => any
      ? Awaited<V>
      : never
    : T;

type A = Awaited<Promise<Promise<Promise<string>>>>;
//   ^? string

Đây là một trong những type recursive phức tạp nhất trong stdlib — sử dụng 4 infer lồng nhau để tách then callback và unwrap.

ThisParameterType<T> & OmitThisParameter<T>

Extract / strip this parameter (chỉ áp dụng cho function dùng function this: X):

type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any
  ? U
  : unknown;

function logName(this: { name: string }) {
  console.log(this.name);
}

type T = ThisParameterType<typeof logName>;
//   ^? { name: string }

Pattern chung của 7 utility trên: dùng conditional kiểm tra hình dạng (function / constructor / promise), rồi đặt infer X đúng vào chỗ cần extract.

Overload: infer chỉ nhìn signature cuối

Một overloaded function không được infer thành union của mọi call signature. Conditional inference dùng signature cuối — thường là implementation-facing signature rộng nhất:

declare function parse(value: string): object;
declare function parse(value: Uint8Array): object;
declare function parse(value: string | Uint8Array): object;

type ParseInput = Parameters<typeof parse>;
// [value: string | Uint8Array]

Nếu cần giữ correlation input/output của từng overload, hãy model bằng union contract hoặc viết overload tương ứng cho wrapper. infer không thực hiện overload resolution dựa trên một argument type tùy ý.


5. Tuple / array manipulation: Head, Tail, Last, Reverse

Tuple type kết hợp với infer cho phép viết “list operation” ở type level — pattern phổ biến trong type-level programming.

Element của array

type ElementOf<T> = T extends readonly (infer U)[] ? U : never;

type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<readonly [1, 'a']>; // 1 | 'a'

Head, Tail, Last, Init

type Head<T extends readonly any[]> = T extends readonly [infer H, ...any[]]
  ? H
  : never;

type Tail<T extends readonly any[]> = T extends readonly [any, ...infer R]
  ? R
  : [];

type Last<T extends readonly any[]> = T extends readonly [...any[], infer L]
  ? L
  : never;

type Init<T extends readonly any[]> = T extends readonly [...infer I, any]
  ? I
  : [];

type H = Head<[1, 2, 3]>; // 1
type T = Tail<[1, 2, 3]>; // [2, 3]
type L = Last<[1, 2, 3]>; // 3
type I = Init<[1, 2, 3]>; // [1, 2]

Reverse (recursive)

type Reverse<T extends readonly any[]> = T extends readonly [
  infer H,
  ...infer R,
]
  ? [...Reverse<R>, H]
  : [];

type R = Reverse<[1, 2, 3, 4]>;
//   ^? [4, 3, 2, 1]

Recursive logic: lấy head, đệ quy tail, append head vào cuối kết quả. Giới hạn instantiation không phải một con số an toàn cố định: nó phụ thuộc hình dạng type, độ rộng union và compiler version. Reverse còn dựng output ở đường quay về nên có thể chạm TS2589 sớm hơn một accumulator-style implementation.

Length của tuple

type Length<T extends readonly any[]> = T['length'];

type L1 = Length<[1, 2, 3]>; // 3
type L2 = Length<string[]>; // number ← không xác định, fallback

Concat (zero-cost ở type level)

type Concat<A extends readonly any[], B extends readonly any[]> = [...A, ...B];

type C = Concat<[1, 2], ['a', 'b']>;
//   ^? [1, 2, 'a', 'b']

6. Function infer: composition, curry, pipe

Thư viện lodash, ramda, effect-ts đều dùng pattern này để type-safe function pipe.

Compose 2 hàm

type Compose<F, G> = F extends (a: infer A) => infer B
  ? G extends (b: B) => infer C
    ? (a: A) => C
    : never
  : never;

declare const toString: (n: number) => string;
declare const toUpper: (s: string) => string;

type Composed = Compose<typeof toString, typeof toUpper>;
//   ^? (a: number) => string

Curry

type Curry<F> = F extends (...args: [infer A, ...infer Rest]) => infer R
  ? Rest extends []
    ? (a: A) => R
    : (a: A) => Curry<(...args: Rest) => R>
  : never;

type Curried = Curry<(a: number, b: string, c: boolean) => User>;
//   ^? (a: number) => (b: string) => (c: boolean) => User

Pipe (variadic)

type Pipe<Fns extends readonly any[], Input> = Fns extends readonly [
  (arg: Input) => infer Next,
  ...infer Rest,
]
  ? Rest extends readonly any[]
    ? Pipe<Rest, Next>
    : never
  : Input;

type Step1 = (n: number) => string;
type Step2 = (s: string) => boolean;
type Step3 = (b: boolean) => null;

type Out = Pipe<[Step1, Step2, Step3], number>;
//   ^? null

Đây là kiểu Type-level recursion: mỗi step lấy first function, tính output, gọi Pipe với rest.


7. Template literal + infer — parse string ở compile time

Đây là tính năng mạnh nhất của infer (xuất hiện từ TS 4.1, 2020). Cho phép pattern-match string literal type, biến string thành dữ liệu có cấu trúc ở compile time.

Cú pháp cơ bản

type StartsWith<S, P extends string> = S extends `${P}${string}` ? true : false;

type T1 = StartsWith<'hello world', 'hello'>; // true
type T2 = StartsWith<'world', 'hello'>; // false

${string} là wildcard match. ${infer X} capture phần match đó.

Split string

type Split<
  S extends string,
  Sep extends string,
> = S extends `${infer Head}${Sep}${infer Rest}`
  ? [Head, ...Split<Rest, Sep>]
  : [S];

type Parts = Split<'a/b/c/d', '/'>;
//   ^? ['a', 'b', 'c', 'd']

Trim

type Whitespace = ' ' | '\n' | '\t';

type TrimLeft<S extends string> = S extends `${Whitespace}${infer R}`
  ? TrimLeft<R>
  : S;

type TrimRight<S extends string> = S extends `${infer R}${Whitespace}`
  ? TrimRight<R>
  : S;

type Trim<S extends string> = TrimLeft<TrimRight<S>>;

type T = Trim<'   hello world   \n'>;
//   ^? 'hello world'

CamelCase ↔ kebab-case

type KebabToCamel<S extends string> = S extends `${infer Head}-${infer Tail}`
  ? `${Head}${Capitalize<KebabToCamel<Tail>>}`
  : S;

type C1 = KebabToCamel<'background-color-primary'>;
//   ^? 'backgroundColorPrimary'

type CamelToKebab<S extends string> = S extends `${infer C}${infer R}`
  ? C extends Lowercase<C>
    ? `${C}${CamelToKebab<R>}`
    : `-${Lowercase<C>}${CamelToKebab<R>}`
  : S;

type K1 = CamelToKebab<'backgroundColorPrimary'>;
//   ^? '-background-color-primary'  ← chú ý '-' đầu, cần helper bỏ

Parse i18n placeholder

type ExtractPlaceholders<S extends string> =
  S extends `${string}{${infer Key}}${infer Rest}`
    ? Key | ExtractPlaceholders<Rest>
    : never;

type Keys = ExtractPlaceholders<'Hello {name}, you have {count} messages'>;
//   ^? 'name' | 'count'

Bây giờ ta có thể bắt buộc caller cung cấp đúng key:

function t<S extends string>(
  template: S,
  args: Record<ExtractPlaceholders<S>, string | number>
): string {
  return template.replace(/\{(\w+)\}/g, (_, k) =>
    String(args[k as keyof typeof args])
  );
}

t('Hello {name}, you have {count} messages', {
  name: 'Vinh',
  count: 3,
}); // ✅

t('Hello {name}', { wrong: 'x' });
// ❌ Type error: missing 'name', 'wrong' is excess

8. Recursive infer — DeepReadonly, Path, Get

infer kết hợp với recursion cho phép walk vào cấu trúc lồng nhau ở compile time.

DeepReadonly

type DeepReadonly<T> = T extends (infer U)[]
  ? readonly DeepReadonly<U>[]
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

type Config = {
  api: { url: string; retry: { count: number } };
  flags: string[];
};

type FrozenConfig = DeepReadonly<Config>;
//   ^? {
//        readonly api: {
//          readonly url: string;
//          readonly retry: { readonly count: number }
//        };
//        readonly flags: readonly string[];
//      }

DeepPartial

type DeepPartial<T> = T extends Function
  ? T
  : T extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T extends object
      ? { [K in keyof T]?: DeepPartial<T[K]> }
      : T;

Path<T> — sinh union các “key.path”

Đây là kỹ thuật nâng cao quan trọng — dùng trong lodash.get type-safe, form library, i18n key:

type Path<T, Prefix extends string = ''> = T extends object
  ? {
      [K in keyof T & string]:
        | (Prefix extends '' ? K : `${Prefix}.${K}`)
        | Path<T[K], Prefix extends '' ? K : `${Prefix}.${K}`>;
    }[keyof T & string]
  : never;

type Translations = {
  home: {
    title: string;
    cta: { primary: string; secondary: string };
  };
  auth: {
    login: string;
    errors: { wrongPassword: string };
  };
};

type Keys = Path<Translations>;
//   ^? 'home' | 'home.title' | 'home.cta' | 'home.cta.primary'
//      | 'home.cta.secondary' | 'auth' | 'auth.login' | 'auth.errors'
//      | 'auth.errors.wrongPassword'

Get<T, P> — lookup theo path

Đôi khi ta chỉ cần leaf path (không có intermediate). Variant:

type LeafPath<T, Prefix extends string = ''> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? LeafPath<T[K], Prefix extends '' ? K : `${Prefix}.${K}`>
        : Prefix extends ''
          ? K
          : `${Prefix}.${K}`;
    }[keyof T & string]
  : never;

type Get<T, P extends string> = P extends `${infer K}.${infer R}`
  ? K extends keyof T
    ? Get<T[K], R>
    : never
  : P extends keyof T
    ? T[P]
    : never;

type V1 = Get<Translations, 'home.cta.primary'>;
//   ^? string

type V2 = Get<Translations, 'home.invalid'>;
//   ^? never

Đây là pattern tRPC dùng để type-safe procedure path, react-hook-form dùng cho field path, i18next dùng cho translation key.


9. infer extends (TS 4.7+) — constrained inference

TS 4.7 thêm infer X extends C — constrain biến infer phải extends một type cho trước. Hữu ích khi muốn narrow ngay trong infer:

// Trước TS 4.7 — phải check sau infer
type ToNumberOld<S> = S extends `${infer N}`
  ? N extends `${number}`
    ? N
    : never
  : never;

// Sau TS 4.7 — gọn hơn, narrow ngay
type ToNumber<S> = S extends `${infer N extends number}` ? N : never;

type N1 = ToNumber<'42'>; // 42
type N2 = ToNumber<'3.14'>; // 3.14
type N3 = ToNumber<'hello'>; // never

Use case: parse stringified JSON literal

type ParseInt<S> = S extends `${infer N extends number}` ? N : never;

type ParseBool<S> = S extends `${infer B extends boolean}` ? B : never;

type R1 = ParseInt<'2026'>; // 2026
type R2 = ParseBool<'true'>; // true
type R3 = ParseBool<'maybe'>; // never

Use case: typed env variable

type EnvNumber<S> = S extends `${infer N extends number}` ? N : number;

declare const PORT: EnvNumber<'3000'>;
//             ^? 3000  ← literal type, chính xác

declare const SOME: EnvNumber<string>;
//             ^? number  ← fallback

infer X extends Constraint không bị giới hạn ở primitive. Constraint có thể là một shape domain bất kỳ:

type Entity = { id: string };

type FirstEntity<T> = T extends readonly [
  infer Head extends Entity,
  ...unknown[],
]
  ? Head
  : never;

Điểm đặc biệt của template literal là compiler có thể parse number, bigintboolean literal từ text khi giá trị round-trip hợp lệ. Đó là behavior của template-string inference, không phải giới hạn chung của constrained infer.


10. 5 use case thực chiến

10.1 Type-safe i18n: parse placeholder ngay từ template

Đã preview ở section 7, đây là phiên bản đầy đủ:

type Placeholders<S extends string> =
  S extends `${string}{${infer K}}${infer R}` ? K | Placeholders<R> : never;

// Hỗ trợ format `{name:string}` `{count:number}` để type-check args
type ParsePlaceholder<S extends string> = S extends `${infer K}:number`
  ? { [P in K]: number }
  : S extends `${infer K}:boolean`
    ? { [P in K]: boolean }
    : { [P in S]: string | number };

type ArgsOf<S extends string> = S extends `${string}{${infer P}}${infer Rest}`
  ? ParsePlaceholder<P> & ArgsOf<Rest>
  : {};

function t<S extends string>(template: S, args: ArgsOf<S>): string {
  return template.replace(/\{([^}]+)\}/g, (_, raw) => {
    const [k] = raw.split(':');
    return String((args as Record<string, unknown>)[k]);
  });
}

t('Hello {name}, age {age:number}', { name: 'Vinh', age: 30 }); // ✅
t('Hello {name}', { name: 123 }); // ✅ (number ok)
// @ts-expect-error — missing 'name'
t('Hello {name}', {});

10.2 Type-safe router: extract params từ path pattern

Pattern này được tanstack-router, next-intl, koa-router dùng:

type ExtractParams<Path extends string> =
  Path extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${string}:${infer Param}`
      ? Param
      : never;

type P1 = ExtractParams<'/users/:userId/posts/:postId'>;
//   ^? 'userId' | 'postId'

function navigate<Path extends string>(
  path: Path,
  params: Record<ExtractParams<Path>, string>
): void {
  // build URL từ pattern + params
}

navigate('/users/:userId/posts/:postId', {
  userId: '42',
  postId: '99',
}); // ✅

// @ts-expect-error — missing 'postId'
navigate('/users/:userId/posts/:postId', { userId: '42' });

10.3 Type-safe RegExp groups

type RegexGroups<R extends string> =
  R extends `${string}(?<${infer Name}>${string})${infer Rest}`
    ? Record<Name, string> & RegexGroups<Rest>
    : {};

function exec<P extends string>(
  pattern: P,
  input: string
): RegexGroups<P> | null {
  const m = input.match(new RegExp(pattern));
  return (m?.groups as RegexGroups<P>) ?? null;
}

const result = exec('(?<year>\\d{4})-(?<month>\\d{2})', '2026-05');
if (result) {
  result.year; // ^? string
  result.month; // ^? string
  // @ts-expect-error — không có group
  result.day;
}

10.4 React component prop extraction

import type { ComponentProps, ComponentType } from 'react';

// ComponentProps tự dùng infer bên trong
type ButtonProps = ComponentProps<typeof MyButton>;
//   ^? props của MyButton

// Custom: extract event handler arg
type EventHandlerArg<T> = T extends (event: infer E) => any ? E : never;

type ClickEvent = EventHandlerArg<React.MouseEventHandler>;
//   ^? React.MouseEvent<Element, MouseEvent>

// Forward ref props extraction
type ForwardedProps<T> =
  T extends React.ForwardRefExoticComponent<infer P> ? P : never;

10.5 State machine event union

type Event =
  | { type: 'LOGIN'; payload: { email: string; password: string } }
  | { type: 'LOGOUT' }
  | { type: 'REFRESH'; payload: { token: string } };

// Extract payload type của 1 event cụ thể
type EventPayload<E extends Event, T extends E['type']> = E extends {
  type: T;
  payload: infer P;
}
  ? P
  : never;

type LoginPayload = EventPayload<Event, 'LOGIN'>;
//   ^? { email: string; password: string }

type LogoutPayload = EventPayload<Event, 'LOGOUT'>;
//   ^? never  ← không có payload

// Type-safe dispatch: event không payload nhận tuple rỗng,
// event có payload nhận tuple một phần tử bắt buộc.
type DispatchArgs<T extends Event['type']> = [EventPayload<Event, T>] extends [
  never,
]
  ? []
  : [payload: EventPayload<Event, T>];

function dispatch<T extends Event['type']>(
  type: T,
  ...args: DispatchArgs<T>
): void {
  // ...
}

dispatch('LOGIN', { email: 'a@b.c', password: 'x' }); // ✅
dispatch('LOGOUT'); // ✅
// @ts-expect-error — REFRESH cần payload
dispatch('REFRESH');

11. Pitfalls, cheat sheet & Kết luận

11.1 Pitfalls thường gặp

Pitfall 1 — infer chỉ bind ở bên trái của conditional

// ❌ Sai
type Bad<T> = T extends infer X ? X : never;

// Cú pháp này không sai về syntax nhưng vô nghĩa — `T extends T ? T : never`
// chỉ là identity. `infer` trong vị trí này không capture gì khác T.

Đặt infer ở vị trí khớp pattern thực sự:

// ✅ Đúng
type ElementOf<T> = T extends Array<infer E> ? E : never;

Pitfall 2 — Distribution unintended

type HasId<T> = T extends { id: unknown } ? true : false;

// Distribution hỏi từng member riêng
type R = HasId<{ id: string } | { name: string }>;
//   ^? true | false

Fix: bọc trong tuple để tắt distribution:

type EveryMemberHasId<T> = [T] extends [{ id: unknown }] ? true : false;

type Every = EveryMemberHasId<{ id: string } | { name: string }>;
//   ^? false

Pitfall 3 — Recursion limit

type Repeat<
  T extends string,
  N extends number,
  Acc extends string = '',
  Count extends unknown[] = [],
> = Count['length'] extends N
  ? Acc
  : Repeat<T, N, `${Acc}${T}`, [...Count, unknown]>;

type R = Repeat<'a', 1000>; // Type instantiation is excessively deep

Type trên vẫn có thể chạm ngân sách instantiation khi N lớn. Accumulator-style recursion thường giữ intermediate type gọn hơn, nhưng không bảo đảm vô hạn và không thay thế benchmark trên compiler/project thật. Một public utility có thể thêm fuel để fallback có chủ đích:

type RepeatWithFuel<
  T extends string,
  N extends number,
  Acc extends string = '',
  Count extends unknown[] = [],
  MaxDepth extends number = 200,
> = Count['length'] extends N
  ? Acc
  : Count['length'] extends MaxDepth
    ? string
    : RepeatWithFuel<T, N, `${Acc}${T}`, [...Count, unknown], MaxDepth>;

Pitfall 4 — Dùng infer identity trong mapped type

// Hợp lệ, nhưng `infer V` không thêm thông tin nào
type RedundantMapped<T> = {
  [K in keyof T]: T[K] extends infer V ? V[] : never;
};

infer hoàn toàn có thể nằm trong conditional type bên trong mapped type. Vấn đề là T[K] extends infer V luôn khớp và V chỉ chính là T[K]; viết T[K][] rõ hơn. Dùng infer khi pattern thật sự bóc một lớp:

type UnwrapProperties<T> = {
  [K in keyof T]: T[K] extends Promise<infer V> ? V : T[K];
};

Pitfall 5 — any “ăn” mọi conditional

type Test<T> = T extends string ? 'yes' : 'no';

type R = Test<any>;
//   ^? 'yes' | 'no'   ← any match cả 2 nhánh!

Khi user pass any, conditional collapse thành union — bug rất khó debug. Defensive: dùng IsAny helper hoặc constrain generic.

11.2 Cheat sheet

Mục đíchPattern
Return type của functionT extends (...args: any) => infer R ? R : never
Args của functionT extends (...args: infer P) => any ? P : never
Element của arrayT extends Array<infer U> ? U : never
Unwrap PromiseT extends Promise<infer V> ? V : T
First / Last của tupleT extends [infer H, ...any[]] ? H : never
Tail / Init của tupleT extends [any, ...infer R] ? R : []
Reverse tuple (recursive)T extends [infer H, ...infer R] ? [...Reverse<R>, H] : []
Split stringS extends `${infer A}${Sep}${infer B}` ? [A, ...] : [S]
Trim whitespaceS extends ` ${infer R}` ? Trim<R> : S (recursive)
Parse number literalS extends `${infer N extends number}` ? N : never
Path<T> recursivedistribute keyof + recurse object value
Tắt distributionbọc trong tuple [T] 2 bên

11.3 Khi nào nên dùng infer?

Nên dùng khi:

  • Viết utility type tái sử dụng (Awaited, EventPayload, ExtractParams)
  • API surface yêu cầu inference từ input của user (form library, router, i18n, RegExp)
  • Có cụm logic infer xuất hiện ≥ 2 lần — refactor thành helper

Không nên lạm dụng khi:

  • Cấu trúc đã rõ ràng — viết type trực tiếp đọc dễ hơn
  • Type quá deep / recursive → IDE chậm, hover lag, error message tệ
  • Có cách viết không-conditional clean hơn (mapped type, simple intersection)

Quy tắc kinh nghiệm: nếu một junior cùng team không hiểu type của bạn trong 30 giây hover, đã đến lúc cân nhắc rút gọn hoặc thêm comment giải thích “vì sao” (xem coding standards của blog này về comment “why”).

11.4 Lab: contract extractor cho SDK

Cho registry sau:

type Endpoint<Method, Path, Body, Response> = {
  method: Method;
  path: Path;
  body: Body;
  response: Response;
};

type Api =
  | Endpoint<'GET', '/users/:id', never, User>
  | Endpoint<'POST', '/users', CreateUser, User>
  | Endpoint<'DELETE', '/users/:id', never, DeleteReceipt>;

Hãy xây:

  1. MethodOf<E>, PathOf<E>, BodyOf<E>, ResponseOf<E> bằng constrained infer;
  2. EndpointFor<Method, Path> giữ correlation, không tạo tích Descartes;
  3. ClientArgs<E> dùng conditional tuple: endpoint có body nhận argument bắt buộc, endpoint never không nhận body;
  4. ClientResult<E> unwrap cả response sync và Promise bằng recursion;
  5. positive tests cho cả ba endpoint và ít nhất sáu @ts-expect-error tests;
  6. một overload test chứng minh Parameters nhìn signature cuối;
  7. một test với any để quyết định fallback về unknown thay vì proof giả;
  8. baseline --extendedDiagnostics trước/sau khi registry tăng từ 10 lên 200 endpoint.

Done khi: caller không viết generic argument, body bắt buộc đúng theo endpoint, response còn correlation, mọi assertion nằm trong runtime adapter đã test, và editor vẫn ở trong performance budget của team.

11.5 Kết luận

infer không phải magic — nó chỉ là một mechanism đơn giản: đặt một type variable ở vị trí cần extract, để compiler unify. Một khi đã internalize điều này, các utility nâng cao trở thành combinator của nguyên lý chung:

  • Conditional cho branching
  • infer cho binding
  • Recursion + template literal cho walking structure
  • infer extends cho constraining

Lần tới khi gặp một API kiểu schema/router hoặc PathOf<TConfig>, bạn có thể đọc nó như một chuỗi pattern match thay vì “type magic”. Nhưng zero runtime instruction không có nghĩa zero cost: recursive inference vẫn tiêu CPU/RAM của compiler, ảnh hưởng editor latency, declaration size và chất lượng diagnostic.

Mục tiêu production không phải extract được mọi thứ có thể extract. Mục tiêu là giữ một correlation có giá trị cho caller, đặt runtime validation đúng boundary và dừng trước khi type program khó vận hành hơn duplication nó loại bỏ.

Tài liệu chính thức