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.
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 — infer là cơ 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, infer là xươ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
Part I — Foundations
- Recap: conditional type
T extends U ? X : Y infer101 — bind type variable trong vị trí bất kỳ- 7 utility built-in dùng
infer
Part II — Intermediate patterns
Part III — Advanced patterns
- Template literal +
infer— parse string ở compile time - Recursive
infer— DeepReadonly, Path, Get infer extends(TS 4.7+) — constrained inference
Part IV — Real-world
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 Xlà “đặ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 choX.
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 T là naked 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, bigint
và boolean 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 đích | Pattern |
|---|---|
| Return type của function | T extends (...args: any) => infer R ? R : never |
| Args của function | T extends (...args: infer P) => any ? P : never |
| Element của array | T extends Array<infer U> ? U : never |
| Unwrap Promise | T extends Promise<infer V> ? V : T |
| First / Last của tuple | T extends [infer H, ...any[]] ? H : never |
| Tail / Init của tuple | T extends [any, ...infer R] ? R : [] |
| Reverse tuple (recursive) | T extends [infer H, ...infer R] ? [...Reverse<R>, H] : [] |
| Split string | S extends `${infer A}${Sep}${infer B}` ? [A, ...] : [S] |
| Trim whitespace | S extends ` ${infer R}` ? Trim<R> : S (recursive) |
| Parse number literal | S extends `${infer N extends number}` ? N : never |
Path<T> recursive | distribute keyof + recurse object value |
| Tắt distribution | bọ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:
MethodOf<E>,PathOf<E>,BodyOf<E>,ResponseOf<E>bằng constrainedinfer;EndpointFor<Method, Path>giữ correlation, không tạo tích Descartes;ClientArgs<E>dùng conditional tuple: endpoint có body nhận argument bắt buộc, endpointneverkhông nhận body;ClientResult<E>unwrap cả response sync và Promise bằng recursion;- positive tests cho cả ba endpoint và ít nhất sáu
@ts-expect-errortests; - một overload test chứng minh
Parametersnhìn signature cuối; - một test với
anyđể quyết định fallback vềunknownthay vì proof giả; - baseline
--extendedDiagnosticstrướ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
infercho binding- Recursion + template literal cho walking structure
infer extendscho 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ỏ.