TypeScript 面试重点不是会不会写类型标注,而是能否用类型系统表达约束、减少运行时错误,并理解类型推导的边界。

类型系统基础

any、unknown 与 never

知识点讲解

any 会关闭类型检查,适合迁移期兜底但不建议滥用。unknown 表示未知类型,使用前必须做类型收窄,更安全。never 表示永远不会出现的值,常用于穷尽检查和抛错函数。

面试常问

问:anyunknown 区别?

答:any 可以赋值给任意类型,也可以任意访问属性,等于放弃检查;unknown 只能赋值给 unknownany,使用前必须判断类型。

类型推断与类型收窄

知识点讲解

TS 会根据初始化值、函数返回值、控制流判断推导类型。常见收窄方式包括 typeofinstanceofin、判空、字面量判断、自定义类型守卫。

面试常问

问:什么是类型守卫?

答:类型守卫是在运行时做判断,并让 TS 在分支内收窄类型的机制。比如 function isString(x: unknown): x is string

泛型

泛型的价值

知识点讲解

泛型让类型像参数一样传递,既保留灵活性,又不丢失类型信息。比如数组取第一个元素,返回值应该和数组元素类型一致,而不是写死成某个类型。

面试常问

问:泛型和 any 有什么区别?

答:泛型保留输入和输出之间的类型关系,any 会丢失关系。泛型是安全的抽象,any 是逃过检查。

泛型约束

知识点讲解

泛型约束用 extends 限制类型参数必须满足某种结构。比如访问 length 时,可以写 T extends { length: number },否则 TS 不知道 T 是否有该属性。

面试常问

问:keyof T 常用在哪里?

答:常用于约束对象属性名。比如 getValue<T, K extends keyof T>(obj: T, key: K): T[K],可以保证 key 一定存在,返回值类型也精确。

高级类型

条件类型与 infer

知识点讲解

条件类型形如 T extends U ? X : Yinfer 可以在条件类型中推断某一部分类型,比如从函数类型中推断返回值。

面试常问

问:如何实现 ReturnType?

答:思路是判断 T 是否是函数类型,如果是,用 infer R 捕获返回值类型:T extends (...args: any[]) => infer R ? R : never

映射类型

知识点讲解

映射类型可以遍历对象类型的 key,批量改变属性。PartialReadonlyPick 等工具类型都基于映射类型实现。

面试常问

问:如何实现 Partial?

答:遍历 keyof T,把每个属性加上可选修饰:type MyPartial<T> = { [K in keyof T]?: T[K] }

联合与交叉

知识点讲解

联合类型表示“可能是其中一种”,使用前通常需要收窄。交叉类型表示“同时满足多个类型”。业务里可用可辨识联合建模状态,比如 loading、success、error。

面试常问

问:为什么可辨识联合适合状态建模?

答:因为每个状态都有明确 type 字段,TS 能根据分支自动收窄,避免访问不存在的数据。

工程实践

工具类型

知识点讲解

常见工具类型包括 PartialRequiredReadonlyPickOmitRecordExcludeExtractReturnTypeParameters。面试要知道作用,也要能讲出实现思路。

面试常问

问:PickOmit 区别?

答:Pick<T, K> 从类型中选出指定 key;Omit<T, K> 从类型中排除指定 key。前者做白名单,后者做黑名单。

tsconfig 常见配置

知识点讲解

strict 是类型安全总开关;noImplicitAny 禁止隐式 any;strictNullChecks 区分 null/undefined;paths 可以配置路径别名;skipLibCheck 会跳过声明文件检查以提升构建速度。

面试常问

问:为什么推荐开启 strict?

答:它能让类型系统更早暴露空值、隐式 any、函数参数等问题。迁移老项目可以分阶段开启,但新项目建议默认开启。

底层追问与代码示例

类型系统是结构化类型

知识点讲解

TypeScript 使用结构化类型系统,判断兼容性时看结构,不看声明来源。这和 Java、C# 这类名义类型系统不同。

1
2
3
4
5
type User = { id: number; name: string }
type Member = { id: number; name: string }

const user: User = { id: 1, name: 'Ada' }
const member: Member = user // OK,结构一致

如果业务上需要区分相同结构的不同概念,可以用品牌类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Brand<T, B extends string> = T & { readonly __brand: B }

type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>

function getUser(id: UserId) {
return id
}

const userId = 'u_1' as UserId
const orderId = 'o_1' as OrderId

getUser(userId)
// getUser(orderId) // 类型错误

分布式条件类型

知识点讲解

当条件类型左侧是裸类型参数时,传入联合类型会自动分发。

1
2
3
4
type ToArray<T> = T extends unknown ? T[] : never

type Result = ToArray<string | number>
// string[] | number[]

如果不想分发,可以用元组包住:

1
2
3
4
type NoDistribute<T> = [T] extends [unknown] ? T[] : never

type Result = NoDistribute<string | number>
// (string | number)[]

面试常问

问:为什么 Exclude 能过滤联合类型?

答:因为它依赖分布式条件类型,对联合类型中的每一项分别判断。

1
2
3
4
type MyExclude<T, U> = T extends U ? never : T

type A = MyExclude<'a' | 'b' | 'c', 'a'>
// 'b' | 'c'

实用类型体操

知识点讲解

中高级面试常要求你写出几个核心工具类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}

type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}

type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}

type MyAwaited<T> =
T extends PromiseLike<infer R> ? MyAwaited<R> : T

as 在映射类型中可以重映射 key,因此能实现 Omit、字段改名、过滤字段。

函数重载与泛型约束

知识点讲解

函数重载适合“输入不同,输出也不同”的 API;泛型适合“输入和输出存在类型关系”的 API。

1
2
3
4
5
6
7
8
function query(id: number): { id: number; type: 'single' }
function query(ids: number[]): { ids: number[]; type: 'batch' }
function query(input: number | number[]) {
if (Array.isArray(input)) {
return { ids: input, type: 'batch' as const }
}
return { id: input, type: 'single' as const }
}

面试常问

问:为什么实现签名不能直接暴露?

答:调用方看到的是重载签名,最后一个实现签名只负责兼容所有重载分支。这样能给调用方更精确的类型。