类型推断
问题
TypeScript 如何进行类型推断?什么是最佳公共类型、上下文类型推断?如何使用 typeof 和 infer?
答案
类型推断是 TypeScript 根据代码上下文自动确定类型的能力。它发生在变量初始化、函数返回值、参数默认值等场景,减少了显式类型注解的需要。
基础类型推断
变量初始化推断
// 字面量推断
let str = 'hello'; // string
let num = 42; // number
let bool = true; // boolean
let nul = null; // null(严格模式下)
// const 会推断为字面量类型
const constStr = 'hello'; // "hello"(字面量类型)
const constNum = 42; // 42(字面量类型)
// 对象推断
const user = {
name: 'Alice',
age: 25
};
// { name: string; age: number }
// 数组推断
const numbers = [1, 2, 3]; // number[]
const mixed = [1, 'hello', true]; // (string | number | boolean)[]
const tuple = [1, 'hello'] as const; // readonly [1, "hello"]
函数返回值推断
// 根据 return 语句推断
function add(a: number, b: number) {
return a + b;
}
// 返回类型推断为 number
function greet(name: string) {
return `Hello, ${name}`;
}
// 返回类型推断为 string
// 多个返回路径
function getValue(flag: boolean) {
if (flag) {
return 'yes';
}
return 42;
}
// 返回类型推断为 string | number
// 异步函数
async function fetchData() {
const response = await fetch('/api');
return response.json();
}
// 返回类型推断为 Promise<any>
最佳公共类型(Best Common Type)
当从多个表达式推断类型时,TypeScript 选择能兼容所有候选类型的类型:
// 数组元素的最佳公共类型
const arr = [1, 2, null];
// (number | null)[]
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const animals = [new Dog(), new Cat()];
// (Dog | Cat)[],不是 Animal[]
// 显式指定基类型
const animals2: Animal[] = [new Dog(), new Cat()];
// Animal[]
上下文类型推断
TypeScript 根据表达式出现的位置推断类型:
函数参数推断
// 数组方法回调
const numbers = [1, 2, 3];
numbers.map((n) => n * 2);
// n 被推断为 number
numbers.filter((n) => n > 2);
// n 被推断为 number
// 事件处理器
document.addEventListener('click', (event) => {
// event 被推断为 MouseEvent
console.log(event.clientX, event.clientY);
});
window.addEventListener('keydown', (event) => {
// event 被推断为 KeyboardEvent
console.log(event.key);
});
回调函数参数
// 函数类型约束参数推断
type Callback = (value: string, index: number) => void;
function process(callback: Callback) {
callback('hello', 0);
}
process((value, index) => {
// value: string, index: number(自动推断)
console.log(value.toUpperCase(), index);
});
赋值推断
// 从赋值目标推断
let handler: (event: MouseEvent) => void;
handler = (e) => {
// e 被推断为 MouseEvent
console.log(e.button);
};
// 对象属性推断
interface Config {
onClick: (x: number, y: number) => void;
}
const config: Config = {
onClick: (x, y) => {
// x, y 被推断为 number
console.log(x + y);
}
};
typeof 类型操作符
使用 typeof 获取值的类型:
const user = {
name: 'Alice',
age: 25,
address: {
city: 'Beijing',
zip: '100000'
}
};
// 获取变量类型
type UserType = typeof user;
// {
// name: string;
// age: number;
// address: { city: string; zip: string }
// }
// 获取函数类型
function createUser(name: string, age: number) {
return { name, age, createdAt: new Date() };
}
type CreateUserFn = typeof createUser;
// (name: string, age: number) => { name: string; age: number; createdAt: Date }
type UserResult = ReturnType<typeof createUser>;
// { name: string; age: number; createdAt: Date }
// 获取常量类型
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];
// 'red' | 'green' | 'blue'
// 获取对象键
type UserKeys = keyof typeof user;
// 'name' | 'age' | 'address'
typeof 与类型守卫
// 运行时类型检查
function process(value: unknown) {
if (typeof value === 'string') {
// value 收窄为 string
return value.toUpperCase();
}
if (typeof value === 'number') {
// value 收窄为 number
return value.toFixed(2);
}
return null;
}
infer 类型推断
在条件类型中使用 infer 推断类型:
函数类型推断
// 推断返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 推断参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// 推断第一个参数
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any
? F
: never;
type Fn = (a: string, b: number) => boolean;
type FirstParam = FirstArg<Fn>; // string
// 推断 this 类型
type ThisType<T> = T extends (this: infer U, ...args: any[]) => any
? U
: never;
数组/元组类型推断
// 推断数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type Elem = ElementType<string[]>; // string
// 推断元组元素
type First<T> = T extends [infer F, ...any[]] ? F : never;
type Last<T> = T extends [...any[], infer L] ? L : never;
type Rest<T> = T extends [any, ...infer R] ? R : never;
type Tuple = [string, number, boolean];
type F = First<Tuple>; // string
type L = Last<Tuple>; // boolean
type R = Rest<Tuple>; // [number, boolean]
Promise 类型推断
// 解包 Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type P1 = Awaited<Promise<string>>; // string
type P2 = Awaited<Promise<Promise<number>>>; // number
type P3 = Awaited<string | Promise<number>>; // string | number
字符串模板推断
// 解析路由参数
type ParseRoute<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ParseRoute<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ParseRoute<'/users/:id/posts/:postId'>;
// 'id' | 'postId'
// 分割字符串
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<'a-b-c', '-'>; // ['a', 'b', 'c']
泛型推断
泛型参数推断
// 从参数推断泛型
function identity<T>(value: T): T {
return value;
}
const str = identity('hello'); // T 推断为 "hello"
const num = identity(42); // T 推断为 42
// 从回调推断
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const result = map([1, 2, 3], (n) => n.toString());
// T 推断为 number, U 推断为 string
// result: string[]
// 多参数推断
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: 'Alice' }, { age: 25 });
// { name: string } & { age: number }
泛型约束与推断
// 约束后推断更精确
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 25 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// 条件约束
function processArray<T extends readonly any[]>(arr: T): T[number] {
return arr[0];
}
const elem = processArray([1, 2, 3] as const); // 1 | 2 | 3
satisfies 与推断
TypeScript 4.9+ 的 satisfies 保留精确推断:
// 使用类型注解:丢失精确类型
const colors1: Record<string, string> = {
red: '#ff0000',
green: '#00ff00'
};
// colors1.red: string
// 使用 satisfies:保留精确类型
const colors2 = {
red: '#ff0000',
green: '#00ff00'
} satisfies Record<string, string>;
// colors2.red: "#ff0000"
// 同时验证和推断
const config = {
api: 'https://api.example.com',
port: 8080,
debug: true
} satisfies Record<string, string | number | boolean>;
// config.api: "https://api.example.com"
// config.port: 8080
// config.debug: true
常见面试问题
Q1: let 和 const 推断的区别?
答案:
// let:推断为基础类型(可变)
let str = 'hello'; // string
let num = 42; // number
let arr = [1, 2, 3]; // number[]
// const:推断为字面量类型(不可变)
const constStr = 'hello'; // "hello"
const constNum = 42; // 42
// 但对象和数组仍推断为可变类型
const obj = { name: 'Alice' }; // { name: string }
const constArr = [1, 2, 3]; // number[]
// 使用 as const 获得完全不可变类型
const tuple = [1, 2, 3] as const; // readonly [1, 2, 3]
const constObj = { name: 'Alice' } as const;
// { readonly name: "Alice" }
Q2: 什么是上下文类型推断?
答案:
// 上下文类型:TypeScript 根据表达式出现的位置推断类型
// 1. 函数参数位置
window.onclick = (event) => {
// event 从 onclick 类型推断为 MouseEvent
console.log(event.button);
};
// 2. 数组方法回调
['a', 'b'].map((item) => {
// item 从数组元素类型推断为 string
return item.toUpperCase();
});
// 3. 对象字面量属性
interface Handlers {
onClick: (x: number, y: number) => void;
onHover: (element: HTMLElement) => void;
}
const handlers: Handlers = {
onClick: (x, y) => {
// x, y 从接口推断为 number
},
onHover: (el) => {
// el 从接口推断为 HTMLElement
}
};
// 4. 返回值推断
function returnsHandler(): (n: number) => string {
return (n) => {
// n 从返回类型推断为 number
return n.toString();
};
}
Q3: 如何使用 typeof 和 keyof 配合?
答案:
// 场景:从运行时值派生类型
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
} as const;
// typeof:获取值的类型
type Config = typeof config;
// {
// readonly apiUrl: "https://api.example.com";
// readonly timeout: 5000;
// readonly retries: 3;
// }
// keyof:获取类型的键
type ConfigKey = keyof typeof config;
// 'apiUrl' | 'timeout' | 'retries'
// 获取值的类型
type ConfigValue = typeof config[ConfigKey];
// "https://api.example.com" | 5000 | 3
// 实际应用:创建 getter 函数
function getConfig<K extends keyof typeof config>(key: K): typeof config[K] {
return config[key];
}
const url = getConfig('apiUrl'); // "https://api.example.com"
const timeout = getConfig('timeout'); // 5000
Q4: infer 和 泛型参数的区别?
答案:
// 泛型参数:调用时确定或显式传入
function identity<T>(value: T): T {
return value;
}
identity<string>('hello'); // T = string
identity(42); // T = 42(推断)
// infer:在条件类型中从类型结构推断
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// infer 无法显式传入,只能被条件类型推断
type Result = GetReturnType<() => string>; // string
// 关键区别:
// 1. 泛型参数可以在调用时指定,infer 只能自动推断
// 2. 泛型参数在整个类型/函数中可用,infer 只在条件为真的分支中可用
// infer 的多位置推断
type FirstAndLast<T> = T extends [infer First, ...any[], infer Last]
? { first: First; last: Last }
: never;
type FL = FirstAndLast<[1, 2, 3, 4]>;
// { first: 1; last: 4 }
Q5: 如何处理复杂的类型推断场景?
答案:
// 场景:类型安全的事件系统
interface EventMap {
click: { x: number; y: number };
focus: { target: HTMLElement };
change: { value: string };
}
// 推断事件类型
type EventHandler<K extends keyof EventMap> = (event: EventMap[K]) => void;
// 创建类型安全的 emit 和 on
class EventEmitter {
private handlers = new Map<string, Function[]>();
on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
const handlers = this.handlers.get(event) || [];
handlers.push(handler);
this.handlers.set(event, handlers);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
const handlers = this.handlers.get(event) || [];
handlers.forEach((h) => h(data));
}
}
const emitter = new EventEmitter();
// 类型自动推断
emitter.on('click', (event) => {
// event 推断为 { x: number; y: number }
console.log(event.x, event.y);
});
emitter.emit('focus', { target: document.body }); // OK
// emitter.emit('click', { value: 'test' }); // 错误:类型不匹配