TypeScript 2026 面试题汇总
一句话定位:聚焦 TypeScript 5.x 类型系统、工程实践与高级类型体操,不收录已废弃的 namespace/enum 滥用等过时模式
目录
基础类型与类型系统
1. TypeScript 的类型系统是结构化的还是名义化的?有什么区别?
答案要点:
- TypeScript 采用结构化类型系统(Structural Typing),也叫"鸭子类型"
- 只要两个类型的结构兼容(属性和方法一致),就认为它们是兼容的,不关心类型名称
- 与 Java/C# 的名义化类型系统(Nominal Typing)不同,后者必须显式声明实现关系
interface Point {
x: number;
y: number;
}
interface Coordinate {
x: number;
y: number;
}
const p: Point = { x: 1, y: 2 };
const c: Coordinate = p; // ✅ 结构兼容,无需显式声明关系追问:如何在 TypeScript 中模拟名义类型?(branded types / unique symbol)
2. any、unknown、never 三者有什么区别?分别在什么场景使用?
答案要点:
any:关闭类型检查,可以赋值给任何类型,也可以接受任何类型。尽量避免使用unknown:类型安全的顶层类型,可以接受任何类型的值,但使用前必须进行类型收窄never:底层类型,表示永远不会出现的值(如抛异常的函数返回值、exhaustive check)
// unknown 必须收窄后才能使用
function process(val: unknown) {
if (typeof val === 'string') {
console.log(val.toUpperCase()); // ✅ 收窄后可用
}
}
// never 用于穷尽检查
type Shape = 'circle' | 'square';
function getArea(shape: Shape) {
switch (shape) {
case 'circle': return Math.PI;
case 'square': return 1;
default:
const _exhaustive: never = shape; // 编译期保证所有分支已覆盖
return _exhaustive;
}
}追问:{} 类型和 object 类型分别表示什么?
3. type 和 interface 有什么区别?什么时候用哪个?
答案要点:
- interface:可以声明合并(declaration merging)、用
extends继承、仅描述对象结构 - type:不可声明合并、用
&交叉组合、可表示联合类型/元组/映射类型等所有类型 - 实践建议:对外暴露的 API 用
interface(方便扩展),内部复杂类型用type
// interface 声明合并
interface Window {
myCustomProp: string;
}
// type 可以表示联合类型
type Result = Success | Failure;
// type 可以表示映射类型
type Readonly<T> = { readonly [K in keyof T]: T[K] };追问:interface 的声明合并在什么场景下有用?(扩展第三方库类型)
4. 元组(Tuple)类型和数组类型有什么区别?
答案要点:
- 数组类型:
string[]或Array<string>,元素类型一致,长度不固定 - 元组类型:
[string, number],每个位置有确定类型,长度固定 - TS 4.0+ 支持可变长元组(Variadic Tuple Types)和命名元组元素
// 基础元组
const pair: [string, number] = ['age', 25];
// 命名元组(提升可读性)
type Range = [start: number, end: number];
// 可变长元组
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
type R = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
// const 断言推导为元组
const colors = ['red', 'green', 'blue'] as const;
// 类型为 readonly ["red", "green", "blue"]追问:as const 断言的作用是什么?与 readonly 有什么关系?
5. 枚举(enum)的底层实现是什么?2026 年还推荐使用吗?
答案要点:
- 数字枚举编译为双向映射对象(值→名称,名称→值),字符串枚举只有单向映射
const enum在编译时内联替换,不生成运行时代码- 2026 年推荐:优先使用
as const对象或联合类型替代 enum,因为:- enum 是 TypeScript 少数生成运行时代码的特性,与"类型擦除"理念不一致
- TC39 正在推进的
--erasableSyntaxOnly模式不支持非 const enum
// ❌ 不推荐:传统 enum
enum Direction {
Up, // 0
Down, // 1
}
// ✅ 推荐:as const + 联合类型
const Direction = {
Up: 'UP',
Down: 'DOWN',
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];
// "UP" | "DOWN"追问:const enum 有什么陷阱?(isolatedModules 下不可用、跨文件问题)
6. TypeScript 中的字面量类型和模板字面量类型是什么?
答案要点:
- 字面量类型:将值本身作为类型,如
'hello'、42、true - 模板字面量类型(Template Literal Types):TS 4.1+ 引入,用模板字符串语法组合字面量类型
- 常用于构建强类型事件名、CSS 属性值、路由路径等
// 模板字面量类型
type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
// "onClick" | "onFocus" | "onBlur"
// 组合生成大量类型
type CSSMargin = `margin-${'top' | 'right' | 'bottom' | 'left'}`;
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"
// 配合 infer 做模式匹配
type ExtractParam<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParam<Rest>
: S extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParam<'/user/:id/post/:postId'>;
// "id" | "postId"追问:模板字面量类型有什么性能限制?(联合类型数量爆炸)
接口与类型别名
7. 交叉类型(Intersection)和联合类型(Union)的区别是什么?
答案要点:
- 联合类型
A | B:值可以是 A 或 B,只能访问共有成员 - 交叉类型
A & B:值同时满足 A 和 B,可以访问所有成员 - 对于原始类型交叉,结果可能是
never(如string & number)
// 联合类型:只能访问共有属性
type Cat = { name: string; meow(): void };
type Dog = { name: string; bark(): void };
function getName(pet: Cat | Dog) {
return pet.name; // ✅ 共有属性
// pet.meow(); // ❌ 不确定是 Cat
}
// 交叉类型:合并所有属性
type CatDog = Cat & Dog;
const hybrid: CatDog = {
name: 'Hybrid',
meow() {},
bark() {},
};追问:交叉类型遇到属性冲突时会发生什么?
8. 如何实现一个类型,让对象的某些属性变为可选?
答案要点:
- 使用
Partial让所有属性可选,但通常只需部分可选 - 组合
Omit+Partial+Pick实现精确控制
// 让指定属性变为可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
// 只有 email 和 avatar 可选
type CreateUserInput = PartialBy<User, 'email' | 'avatar'>;
// { id: number; name: string; email?: string; avatar?: string }追问:Required 和 Partial 的实现原理?(映射类型 + -? 修饰符)
9. 索引签名和 Record 工具类型的区别?
答案要点:
- 索引签名:
{ [key: string]: T },键类型只能是string、number或symbol Record<K, V>:{ [P in K]: V },键可以是联合类型字面量,更精确- TS 4.4+ 支持 Symbol 和模板字面量作为索引签名
// 索引签名:键范围宽泛
interface StringMap {
[key: string]: number;
}
// Record:键是精确的联合类型
type Fruit = 'apple' | 'banana' | 'cherry';
const prices: Record<Fruit, number> = {
apple: 3,
banana: 2,
cherry: 5,
};
// 漏掉任何一个 key 都会报错 ✅追问:索引签名和明确属性共存时有什么约束?
泛型
10. 什么是泛型?为什么需要泛型?
答案要点:
- 泛型是参数化类型,允许在定义函数/类/接口时不预先指定具体类型,使用时再确定
- 核心价值:在保持类型安全的同时实现代码复用
- 没有泛型,要么丢失类型信息(用
any),要么为每种类型写重复代码
// 没有泛型:丢失类型信息
function firstAny(arr: any[]): any {
return arr[0];
}
// 有泛型:保持类型安全
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // number | undefined
const str = first(['a', 'b']); // string | undefined追问:泛型参数的默认值怎么用?(<T = string>)
11. 泛型约束(Generic Constraints)怎么用?extends 在泛型中的含义?
答案要点:
T extends U表示 T 必须是 U 的子类型,用于限制泛型的范围- 常见约束模式:
extends object、extends string、extends { length: number } keyof+extends组合可以约束属性名
// 约束 T 必须有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('hello', 'hi'); // ✅ string 有 length
longest([1, 2], [1, 2, 3]); // ✅ 数组有 length
// longest(10, 20); // ❌ number 没有 length
// keyof 约束属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 25 };
getProperty(user, 'name'); // string
// getProperty(user, 'foo'); // ❌ 'foo' 不是 keyof User追问:extends 在条件类型中是什么含义?(是"可赋值给"而非"继承")
12. 什么是泛型推断(infer)?给出典型使用场景
答案要点:
infer只能在条件类型的extends子句中使用,用于声明一个待推断的类型变量- 常用于提取函数返回值、Promise 内部类型、数组元素类型等
// 提取函数返回类型(ReturnType 的实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean
// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type R3 = UnwrapPromise<Promise<Promise<number>>>; // number
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type R4 = ElementOf<string[]>; // string
// 提取函数第一个参数类型
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type R5 = FirstArg<(name: string, age: number) => void>; // string追问:infer 在同一位置多次推断同一变量时会怎样?(协变位置取联合,逆变位置取交叉)
13. 泛型在 React 组件中怎么使用?
答案要点:
- 泛型组件让 props 类型可以参数化,提升组件复用性
- 常见场景:通用列表组件、表单组件、Select 组件
// 泛型列表组件
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用时自动推断 T
<List
items={[{ id: 1, name: 'Alice' }]}
renderItem={user => <span>{user.name}</span>}
keyExtractor={user => String(user.id)}
/>追问:forwardRef 如何与泛型组件配合?(TS 5.x 中直接支持)
类型守卫与类型收窄
14. TypeScript 有哪些类型收窄(Type Narrowing)的方式?
答案要点:
typeof:收窄原始类型(string/number/boolean/symbol/bigint/undefined/function)instanceof:收窄类实例in:检查属性是否存在- 等值检查:
=== null、=== undefined - 可辨识联合(Discriminated Union):通过共有字面量属性区分
- 自定义类型谓词:
is关键字 satisfies:TS 4.9+,验证类型但不改变推断
// 可辨识联合 —— 最推荐的模式
type Success = { status: 'ok'; data: string };
type Failure = { status: 'error'; message: string };
type Result = Success | Failure;
function handle(result: Result) {
if (result.status === 'ok') {
console.log(result.data); // ✅ 收窄为 Success
} else {
console.log(result.message); // ✅ 收窄为 Failure
}
}追问:typeof null 返回什么?TypeScript 如何处理这个 JS 历史遗留问题?
15. 自定义类型守卫(Type Predicate)怎么写?什么时候需要?
答案要点:
- 返回类型标注为
paramName is Type,告诉编译器函数返回 true 时参数是指定类型 - 当 TypeScript 无法自动收窄时使用,如跨函数调用、复杂对象判断
interface Fish { swim(): void }
interface Bird { fly(): void }
// 自定义类型守卫
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // ✅ 收窄为 Fish
} else {
pet.fly(); // ✅ 收窄为 Bird
}
}
// 配合 filter 使用(过滤 null/undefined)
const values: (string | null)[] = ['a', null, 'b'];
const nonNull = values.filter((v): v is string => v !== null);
// nonNull: string[]追问:asserts 关键字和 is 有什么区别?(assertion functions)
16. satisfies 运算符的作用是什么?和类型标注有什么区别?
答案要点:
satisfies验证表达式符合某个类型,但保留原始推断类型,不拓宽- 类型标注
:Type会将变量类型固定为声明的类型,丢失字面量信息 - TS 4.9+ 引入,解决了"既要类型检查,又要保留推断"的矛盾
type Colors = Record<string, [number, number, number] | string>;
// 用类型标注:丢失具体信息
const colorsA: Colors = {
red: [255, 0, 0],
green: '#00ff00',
};
// colorsA.red 的类型是 [number, number, number] | string ❌ 不精确
// 用 satisfies:保留推断
const colorsB = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Colors;
// colorsB.red 的类型是 [number, number, number] ✅ 精确
// colorsB.green 的类型是 string ✅ 精确追问:satisfies + as const 组合使用的场景?
高级类型
17. 条件类型(Conditional Types)怎么理解?什么是分布式条件类型?
答案要点:
- 条件类型:
T extends U ? X : Y,根据类型关系选择不同分支 - 分布式条件类型:当 T 是联合类型且为裸类型参数时,会自动分发到每个成员
- 可以用
[T] extends [U]包裹来阻止分发
// 基础条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<number>; // false
// 分布式条件类型
type Exclude<T, U> = T extends U ? never : T;
type C = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
// 等价于:('a' extends 'a' ? never : 'a') | ('b' extends 'a' ? never : 'b') | ('c' extends 'a' ? never : 'c')
// 阻止分发
type IsNever<T> = [T] extends [never] ? true : false;
type D = IsNever<never>; // true(如果不包裹,never 分发后什么都不剩,结果是 never)追问:never 在条件类型中的分发行为是什么?
18. 映射类型(Mapped Types)是什么?如何使用修饰符?
答案要点:
- 映射类型:通过
in keyof遍历已有类型的键来生成新类型 - 修饰符:
readonly、?,可以用+/-前缀来添加或移除 - 键重映射(Key Remapping):TS 4.1+ 通过
as子句改变键名
// 基础映射类型
type Optional<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] }; // 移除可选
type ReadonlyAll<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 移除只读
// 键重映射:Getter 类型
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// 过滤键
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
};
type WithoutStrings = OmitByType<Person, string>;
// { age: number }追问:映射类型中 keyof T 和 in 的关系是什么?
19. 协变和逆变是什么?在 TypeScript 中怎么体现?
答案要点:
- 协变(Covariance):子类型关系保持方向。
Dog extends Animal→Array<Dog> extends Array<Animal> - 逆变(Contravariance):子类型关系反转。出现在函数参数位置
- 双变(Bivariance):TypeScript 默认函数参数是双变的,开启
strictFunctionTypes后变为逆变 - 不变(Invariance):既不协变也不逆变
class Animal { name = '' }
class Dog extends Animal { bark() {} }
// 协变:返回值位置
type Producer<T> = () => T;
let produceDog: Producer<Dog> = () => new Dog();
let produceAnimal: Producer<Animal> = produceDog; // ✅ 协变
// 逆变:参数位置(strictFunctionTypes: true)
type Consumer<T> = (arg: T) => void;
let consumeAnimal: Consumer<Animal> = (a: Animal) => {};
let consumeDog: Consumer<Dog> = consumeAnimal; // ✅ 逆变
// consumeAnimal = consumeDog; // ❌ 反过来不行
// 利用逆变实现 UnionToIntersection
type UnionToIntersection<U> =
(U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
? I
: never;
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }追问:为什么 strictFunctionTypes 只对函数表达式生效,对方法声明不生效?
20. 什么是 Branded Types(品牌类型)?怎么实现?
答案要点:
- TypeScript 的结构类型系统下,结构相同的类型可以互相赋值
- Branded Types 通过添加一个虚拟的品牌属性,实现运行时无开销的名义类型
- 常用于:用户 ID vs 订单 ID、像素 vs 百分比等需要区分的同基础类型场景
// 声明品牌类型
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
// 创建函数(运行时零开销)
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
function getUser(id: UserId) { /* ... */ }
const userId = createUserId('user_123');
const orderId = createOrderId('order_456');
getUser(userId); // ✅
// getUser(orderId); // ❌ OrderId 不能赋值给 UserId追问:除了 __brand,还有其他实现品牌类型的方式吗?(unique symbol)
工具类型
21. TypeScript 内置工具类型有哪些?实现原理?
答案要点:
常用内置工具类型及其实现:
// 1. Partial:所有属性变可选
type Partial<T> = { [K in keyof T]?: T[K] };
// 2. Required:所有属性变必选
type Required<T> = { [K in keyof T]-?: T[K] };
// 3. Readonly:所有属性变只读
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// 4. Pick:选取指定属性
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// 5. Omit:排除指定属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 6. Record:构造键值对类型
type Record<K extends keyof any, T> = { [P in K]: T };
// 7. Exclude:从联合类型中排除
type Exclude<T, U> = T extends U ? never : T;
// 8. Extract:从联合类型中提取
type Extract<T, U> = T extends U ? T : never;
// 9. NonNullable:排除 null 和 undefined
type NonNullable<T> = T & {};
// 10. ReturnType:提取函数返回类型
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
// 11. Parameters:提取函数参数类型
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
// 12. Awaited:递归解包 Promise(TS 4.5+)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;追问:NonNullable 从 Exclude<T, null | undefined> 改为 T & {} 的原因?
22. Omit 和 Pick 有什么区别?如何实现深层 Partial?
答案要点:
Pick<T, K>选取属性,Omit<T, K>排除属性,互为补集- 深层 Partial 需要递归映射
// 深层 Partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Config {
db: {
host: string;
port: number;
options: {
ssl: boolean;
timeout: number;
};
};
log: {
level: string;
};
}
// 所有层级的属性都变为可选
type PartialConfig = DeepPartial<Config>;
// db?: { host?: string; port?: number; options?: { ssl?: boolean; timeout?: number } }
// 深层 Readonly
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;追问:递归类型有深度限制吗?(TS 有内置的递归深度限制,约 50 层)
23. 如何实现 StrictOmit,让 K 必须是 T 的已知键?
答案要点:
- 内置
Omit<T, K>中 K 约束为keyof any(即string | number | symbol),不会报错即使 K 不在 T 中 StrictOmit将 K 约束为keyof T,传入不存在的键会编译报错
// 内置 Omit 的问题
interface User { name: string; age: number }
type T1 = Omit<User, 'foo'>; // 不报错,但 'foo' 不是 User 的键 ❌
// StrictOmit
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type T2 = StrictOmit<User, 'name'>; // ✅ { age: number }
// type T3 = StrictOmit<User, 'foo'>; // ❌ 编译错误追问:在什么场景下需要 StrictOmit?
类型体操
24. 如何实现 UnionToIntersection(联合转交叉)?
答案要点:
- 利用函数参数的逆变特性:将联合类型分发到函数参数位置,再用
infer推断 - 逆变位置的多个候选类型会被推断为交叉类型
type UnionToIntersection<U> =
(U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
? I
: never;
// 步骤分解:
// 1. U = A | B
// 2. 分发:(arg: A) => void | (arg: B) => void
// 3. 推断:infer I 在逆变位置,A & B
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }追问:能实现 IntersectionToUnion 吗?(不能,TS 无法反向拆解交叉类型)
25. 如何实现 TupleToUnion(元组转联合)?
答案要点:
- 使用索引访问
T[number]即可将元组所有元素类型转为联合
type TupleToUnion<T extends readonly any[]> = T[number];
type T1 = TupleToUnion<[string, number, boolean]>;
// string | number | boolean
// 结合 as const 使用
const STATUS = ['active', 'inactive', 'pending'] as const;
type Status = TupleToUnion<typeof STATUS>;
// "active" | "inactive" | "pending"追问:如何实现 UnionToTuple?(非常复杂,需要利用函数重载推断的顺序特性)
26. 如何实现一个 DeepRequired 类型?
答案要点:
- 递归遍历所有属性,用
-?移除可选修饰符 - 需要处理数组、函数等特殊情况
type DeepRequired<T> = T extends (...args: any[]) => any
? T // 函数不递归
: T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
interface FormData {
user?: {
name?: string;
address?: {
city?: string;
zip?: string;
};
};
tags?: string[];
}
type RequiredForm = DeepRequired<FormData>;
// { user: { name: string; address: { city: string; zip: string } }; tags: string[] }追问:如何实现只递归 N 层的 DeepPartial?
27. 如何实现路径类型(Path Types),提取对象的所有嵌套路径?
答案要点:
- 递归遍历对象键,用模板字面量类型拼接路径
- 实际框架如 react-hook-form、lodash 的
get方法都使用类似技术
type Path<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: K | `${K}.${Path<T[K]>}`
}[keyof T & string]
: never;
interface User {
name: string;
address: {
city: string;
zip: string;
};
tags: string[];
}
type UserPaths = Path<User>;
// "name" | "address" | "address.city" | "address.zip" | "tags"
// 根据路径获取值类型
type PathValue<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
type CityType = PathValue<User, 'address.city'>; // string追问:这种递归类型在属性很多时会有什么性能问题?
28. 实现一个 Trim<S> 类型,去除字符串类型两端的空格
答案要点:
- 利用模板字面量类型 + 递归,逐个字符匹配空格并去除
type Whitespace = ' ' | '\n' | '\t';
type TrimLeft<S extends string> =
S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S;
type TrimRight<S extends string> =
S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S;
type Trim<S extends string> = TrimLeft<TrimRight<S>>;
type R1 = Trim<' hello '>; // "hello"
type R2 = Trim<'\n world \t'>; // "world"追问:如何实现 Replace<S, From, To> 和 ReplaceAll?
模块与声明文件
29. .d.ts 声明文件的作用是什么?declare 关键字什么时候用?
答案要点:
.d.ts文件:只包含类型声明,不包含实现代码,用于为 JS 库提供类型信息declare:声明一个已在运行时存在但 TS 不知道的变量/函数/类/模块- 三种来源:内置(
lib.d.ts)、@types/*包、项目自定义
// 声明全局变量(如 CDN 引入的库)
declare const gtag: (...args: any[]) => void;
// 声明模块(为无类型的 npm 包补类型)
declare module 'untyped-lib' {
export function doSomething(input: string): number;
}
// 声明全局类型扩展
declare global {
interface Window {
__APP_VERSION__: string;
}
}
// 声明 CSS Modules
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
// 声明图片等资源
declare module '*.png' {
const src: string;
export default src;
}追问:declare module 和 declare namespace 的区别?
30. TypeScript 的模块解析策略有哪些?moduleResolution 怎么配?
答案要点:
node(Node.js 经典策略):按node_modules查找,支持index.tsnode16/nodenext:支持 ESM + CJS 双模式,package.json的exports字段bundler(TS 5.0+,推荐):适用于打包器环境,支持exports但不强制文件扩展名
// tsconfig.json 推荐配置(2026)
{
"compilerOptions": {
"moduleResolution": "bundler",
"module": "esnext",
"target": "esnext",
"verbatimModuleSyntax": true
}
}verbatimModuleSyntax(TS 5.0+):取代isolatedModules,要求import type显式标记类型导入
// ✅ 正确:类型导入必须用 import type
import type { User } from './types';
import { createUser } from './utils';
// ❌ 错误:不使用 import type 导入纯类型
import { User } from './types'; // verbatimModuleSyntax 报错追问:package.json 的 exports 字段如何影响 TypeScript 类型解析?
31. 如何为第三方 JS 库编写类型声明?
答案要点:
- 优先查看 DefinitelyTyped(
@types/*),没有再手写 - 创建
*.d.ts文件,使用declare module声明模块类型
// types/awesome-lib.d.ts
declare module 'awesome-lib' {
interface Options {
timeout?: number;
retries?: number;
}
interface Result {
data: unknown;
status: number;
}
// 默认导出
export default function awesomeLib(url: string, options?: Options): Promise<Result>;
// 命名导出
export function configure(options: Options): void;
export const version: string;
}- 在
tsconfig.json中配置typeRoots或paths确保能找到声明文件
追问:@types/* 包的版本如何与库版本对应?
TypeScript 编译与配置
32. tsconfig.json 中最重要的配置项有哪些?2026 年推荐怎么配?
答案要点:
strict: true:开启所有严格类型检查(必须)target:编译目标,现代项目用ES2022或ESNextmodule:模块系统,推荐ESNextmoduleResolution:推荐bundler(打包器环境)verbatimModuleSyntax:强制import type语法skipLibCheck:跳过.d.ts文件检查,加速编译
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}追问:noUncheckedIndexedAccess 有什么作用?(索引访问返回 T | undefined)
33. strict 模式具体包含哪些子选项?
答案要点:
strict: true 等价于同时开启以下所有选项:
strictNullChecks:null/undefined不能赋值给其他类型strictFunctionTypes:函数参数严格逆变检查strictBindCallApply:bind/call/apply参数严格检查strictPropertyInitialization:类属性必须初始化或在构造函数中赋值noImplicitAny:禁止隐式anynoImplicitThis:禁止隐式this类型为anyuseUnknownInCatchVariables:catch的error参数类型为unknown而非anyalwaysStrict:生成代码始终使用严格模式
// useUnknownInCatchVariables: true
try {
throw new Error('fail');
} catch (e) {
// e 是 unknown,必须收窄
if (e instanceof Error) {
console.log(e.message); // ✅
}
}追问:exactOptionalPropertyTypes 和 strictNullChecks 的区别?
34. TypeScript 的编译流程是怎样的?tsc 做了什么?
答案要点:
- Scanner(扫描器):源码 → Token 流
- Parser(解析器):Token → AST(抽象语法树)
- Binder(绑定器):AST → Symbol Table(符号表),建立作用域
- Checker(检查器):类型检查,最核心最耗时的阶段
- Emitter(发射器):AST → JavaScript +
.d.ts+ SourceMap
源码(.ts) → Scanner → Parser → AST
↓
Binder → Symbols
↓
Checker → 类型检查
↓
Emitter → .js + .d.ts + .map- 类型擦除:所有类型标注在编译后被移除,运行时不存在 TypeScript 类型信息
- TS 5.8+ 的
--erasableSyntaxOnly确保只使用可擦除语法
追问:为什么说 TypeScript 的 Checker 是最复杂的部分?(图灵完备的类型系统)
35. 项目引用(Project References)和增量编译是什么?
答案要点:
- 项目引用:
references字段将大项目拆分为多个子 tsconfig,实现独立编译和缓存 - 增量编译:
incremental: true+.tsbuildinfo文件,只重新编译变更的部分 composite: true:启用项目引用的子项目必须开启
// tsconfig.json(根项目)
{
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/api" }
]
}
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"incremental": true,
"declaration": true,
"outDir": "./dist"
}
}- 使用
tsc --build(tsc -b)编译,自动处理依赖顺序 - Monorepo 项目中大幅提升编译速度
追问:tsc --build 和直接 tsc 的区别?
TypeScript 5.x 新特性
36. TypeScript 5.0-5.7 带来了哪些重要新特性?
答案要点:
TS 5.0:
- 装饰器(Decorators):符合 TC39 Stage 3 标准的新装饰器语法
const类型参数:<const T>让泛型参数自动推断为字面量类型moduleResolution: bundler:新的模块解析策略
TS 5.1:
- 函数返回
undefined时可以不写return语句 getter/setter可以有不同类型
TS 5.2:
using声明:确定性资源管理(Explicit Resource Management)- 装饰器元数据
TS 5.3:
import属性语法(import ... with { type: 'json' })switch(true)类型收窄
TS 5.4:
NoInfer<T>工具类型:阻止特定位置的类型推断- 闭包中的类型收窄改进
TS 5.5:
- 推断的类型谓词:自动推导
filter回调为类型守卫 - 正则表达式语法检查
TS 5.6:
- 迭代器辅助方法(
Iterator.map/filter等) --noUncheckedSideEffectImports
TS 5.7:
--rewriteRelativeImportExtensions:自动重写导入路径扩展名- 初始化变量的类型收窄改进
追问:using 声明和 try/finally 相比有什么优势?
37. const 类型参数(<const T>)是什么?解决了什么问题?
答案要点:
- 在泛型参数前加
const,让推断结果自动变为字面量/只读类型 - 等价于调用方写
as const,但对调用方更友好
// 没有 const:推断为宽泛类型
function routes<T extends Record<string, string>>(config: T) {
return config;
}
const r1 = routes({ home: '/', about: '/about' });
// T = { home: string; about: string } ❌ 丢失字面量
// 有 const:自动推断为字面量
function routesConst<const T extends Record<string, string>>(config: T) {
return config;
}
const r2 = routesConst({ home: '/', about: '/about' });
// T = { readonly home: "/"; readonly about: "/about" } ✅ 保留字面量追问:<const T> 和 satisfies 有什么异同?
38. using 声明(资源管理)是什么?怎么用?
答案要点:
- TC39 Stage 3 提案(
Symbol.dispose/Symbol.asyncDispose),TS 5.2+ 支持 - 类似 C# 的
using、Python 的with、Go 的defer - 作用域结束时自动调用
[Symbol.dispose](),确保资源释放
// 定义可释放资源
class DatabaseConnection implements Disposable {
constructor(private url: string) {
console.log(`Connected to ${url}`);
}
query(sql: string) {
return []; // 模拟查询
}
[Symbol.dispose]() {
console.log(`Disconnected from ${this.url}`);
}
}
// 使用 using 声明
function fetchData() {
using db = new DatabaseConnection('postgres://localhost/mydb');
const data = db.query('SELECT * FROM users');
return data;
// 函数结束自动调用 db[Symbol.dispose]()
}
// 异步版本
class AsyncFileHandle implements AsyncDisposable {
async [Symbol.asyncDispose]() {
// 异步清理
}
}
async function processFile() {
await using handle = new AsyncFileHandle();
// ...
// 自动 await handle[Symbol.asyncDispose]()
}追问:using 和 try/finally 相比有什么优势?(多资源场景更简洁,避免嵌套)
39. NoInfer<T> 工具类型解决什么问题?
答案要点:
- TS 5.4+ 内置,阻止 TypeScript 从某个位置推断泛型参数
- 常用于:当一个泛型参数出现在多个位置时,控制推断优先级
// 问题:默认值位置也参与推断,导致类型过宽
function createState<T>(initial: T, defaultValue: T) {
return { value: initial, default: defaultValue };
}
// T 被推断为 string,但我们想让 defaultValue 跟随 initial 的推断
createState('hello', 42); // ❌ 应该报错但可能不报错
// 解决:用 NoInfer 阻止 defaultValue 参与推断
function createStateBetter<T>(initial: T, defaultValue: NoInfer<T>) {
return { value: initial, default: defaultValue };
}
createStateBetter('hello', 42); // ❌ number 不能赋值给 string ✅
// 常见场景:事件处理器
function on<T extends string>(event: T, callback: (data: NoInfer<T>) => void) {
// T 只从 event 参数推断,callback 中的 T 不参与推断
}追问:NoInfer 的实现原理?(利用条件类型阻断推断链)
工程实践
40. TypeScript 项目中如何处理 any 类型?有哪些替代方案?
答案要点:
- 原则:
any是类型系统的逃生舱,应尽量避免 - 替代方案:
unknown— 安全的顶层类型,需收窄后使用- 泛型 — 保持类型信息的同时实现灵活性
- 函数重载 — 处理多种参数组合
satisfies— 既要检查又要保留推断
// ❌ 常见的 any 滥用
function parse(input: any): any {
return JSON.parse(input);
}
// ✅ 用泛型 + unknown 替代
function parse<T>(input: string): T {
return JSON.parse(input) as T;
}
// ✅ 更安全:运行时校验(配合 zod)
import { z } from 'zod';
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(input: string): User {
return UserSchema.parse(JSON.parse(input));
}- ESLint 配置:
@typescript-eslint/no-explicit-any配合@typescript-eslint/no-unsafe-*系列规则
追问:// @ts-ignore 和 // @ts-expect-error 的区别?
41. TypeScript 与 Zod/Valibot 等运行时校验库如何配合?
答案要点:
- TypeScript 类型只在编译期存在,运行时被擦除,无法校验 API 返回值
- 运行时校验库(Zod、Valibot、ArkType)解决了编译期与运行时的类型同步问题
- 核心模式:Schema → 推导出 TypeScript 类型,一处定义两处使用
import { z } from 'zod';
// 1. 定义 Schema(单一来源)
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.coerce.date(),
});
// 2. 推导 TypeScript 类型
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "user" | "guest"; createdAt: Date }
// 3. 运行时安全解析
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return UserSchema.parse(data); // 运行时校验,失败抛异常
}
// 4. Valibot(更轻量的替代方案,tree-shakable)
import * as v from 'valibot';
const UserSchema2 = v.object({
id: v.pipe(v.string(), v.uuid()),
name: v.pipe(v.string(), v.minLength(1)),
email: v.pipe(v.string(), v.email()),
});追问:Zod 和 Valibot 的核心区别?(包体积、API 设计、tree-shaking)
42. 如何在大型项目中组织 TypeScript 类型?
答案要点:
- 按功能模块组织,而非集中在一个
types/目录 - 类型就近原则:类型定义靠近使用处
- 共享类型放在
shared/types或@types/中
src/
├── features/
│ ├── user/
│ │ ├── types.ts # User 相关类型
│ │ ├── api.ts
│ │ └── components/
│ └── order/
│ ├── types.ts # Order 相关类型
│ └── api.ts
├── shared/
│ ├── types/
│ │ ├── api.ts # 通用 API 类型(Response、Pagination)
│ │ └── common.ts # 通用工具类型
│ └── utils/
└── types/
└── env.d.ts # 环境变量、全局声明- 导出策略:
- 模块内部类型不导出(
type不加export) - 跨模块共享类型通过 barrel file(
index.ts)统一导出 - 使用
import type明确区分类型导入和值导入
- 模块内部类型不导出(
追问:type 导出和 interface 导出在打包器中的区别?
43. TypeScript 中如何实现类型安全的 API 层?
答案要点:
- 使用泛型封装请求函数,实现端到端类型安全
- 配合路由定义,确保请求参数和响应类型匹配
// 定义 API 路由类型映射
interface ApiRoutes {
'GET /users': {
query: { page: number; limit: number };
response: { users: User[]; total: number };
};
'GET /users/:id': {
params: { id: string };
response: User;
};
'POST /users': {
body: CreateUserInput;
response: User;
};
}
// 类型安全的请求函数
async function api<T extends keyof ApiRoutes>(
route: T,
config: Omit<ApiRoutes[T], 'response'>
): Promise<ApiRoutes[T]['response']> {
// 实现...
const [method, path] = (route as string).split(' ');
const res = await fetch(path, { method });
return res.json();
}
// 使用时完全类型安全
const { users, total } = await api('GET /users', {
query: { page: 1, limit: 10 },
});
// users: User[], total: number ✅追问:tRPC 是如何实现端到端类型安全的?
44. 装饰器(Decorators)在 TypeScript 5.x 中有什么变化?
答案要点:
- TS 5.0 实现了 TC39 Stage 3 标准装饰器,与旧版实验性装饰器(
experimentalDecorators)不同 - 新装饰器是纯函数,接收
(target, context)两个参数 - 不再需要
reflect-metadata,语义更清晰
// 新标准装饰器(TS 5.0+)
function log<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
) {
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
console.log(`Calling ${String(context.name)} with`, args);
const result = target.apply(this, args);
console.log(`Result:`, result);
return result;
} as T;
}
class Calculator {
@log
add(a: number, b: number) {
return a + b;
}
}
// 类装饰器
function sealed(target: Function, _context: ClassDecoratorContext) {
Object.seal(target);
Object.seal(target.prototype);
}
@sealed
class Greeter {
greet() { return 'Hello'; }
}追问:新装饰器和旧版 experimentalDecorators 能共存吗?(不能,需要迁移)
45. TypeScript 性能优化有哪些技巧?
答案要点:
类型层面:
- 避免过度复杂的条件类型和递归类型
- 使用
interface而非type(interface 有缓存优化) - 减少联合类型的成员数量
- 避免
keyof大型类型
编译层面:
- 开启
incremental和tsBuildInfo - 使用项目引用拆分大项目
skipLibCheck: true跳过第三方类型检查
- 开启
工具层面:
- 使用
@typescript-eslint/parser替代 TSLint(已废弃) - 考虑 SWC / esbuild 做转译(只做类型擦除,不做类型检查)
tsc --noEmit只做类型检查,转译交给更快的工具
- 使用
# 分析 TypeScript 编译性能
tsc --generateTrace ./trace
# 用 Chrome DevTools 的 Performance 面板打开 trace 文件分析
# 只做类型检查(CI 中常用)
tsc --noEmit
# 用 SWC 做快速转译
npx swc src -d dist追问:为什么 interface extends 比 type & 交叉类型性能更好?