TypeScript 面试题
TypeScript 面试题测试你对静态类型、类型推断和高级类型系统特性的理解。它们通常用于前端、后端和全栈岗位,以确保你能编写可扩展、可维护的代码。预计会有概念解释和动手编码问题。
TypeScript 面试涵盖内容
类型与接口
理解类型别名和接口的区别,以及何时使用它们。涵盖对象类型、函数签名和声明合并。
泛型
了解如何使用泛型参数创建可重用、类型安全的组件和函数。包括约束和默认类型。
类型推断与收窄
演示 TypeScript 如何推断类型并使用条件、判别联合和类型守卫收窄类型。
工具类型与条件类型
利用内置工具类型(例如 Partial、Pick)并创建自定义条件类型来动态转换类型。
TypeScript 面试题示例
- 解释 TypeScript 中 `type` 和 `interface` 的区别。什么时候你会使用其中一个而不是另一个?好回答应覆盖
- interface 支持声明合并(declaration merging),而 type 不支持。
- interface 只能定义对象类型,而 type 可以定义联合类型、交叉类型等。
- type 可以使用 mapped types、conditional types 等高级特性。
- 当需要扩展(extends)时,interface 更直观,type 使用交叉类型。
查看范例答案
在 TypeScript 中,type 和 interface 都可以用来定义对象形状,但存在关键区别。Interface 支持声明合并,即同名的 interface 会自动合并属性,这在扩展第三方库类型时很有用。而 type 不支持合并,但可以通过联合(|)、交叉(&)类型创建更复杂的类型。Interface 更适用于定义公共 API 的契约,因为其扩展性更好;type 则适合需要联合类型、元组、函数签名或映射类型的场景。通常,当定义面向对象的类或库(像 React Props)时优先选择 interface,而定义工具类型(如 Partial、Readonly)或复杂类型别名时使用 type。需要注意的是,两者都可以被类实现(implements),但 interface 更符合传统 OOP 风格。
- 实现一个泛型 `DeepReadonly<T>` 类型,使所有属性和嵌套属性变为只读。好回答应覆盖
- DeepReadonly 需要递归地将所有属性及嵌套属性设为 readonly。
- 使用递归条件类型,检查属性是否为对象类型。
- 对于数组或元组也需要处理。
- 注意循环引用可能导致无限递归,但 TypeScript 有深度限制。
查看范例答案
DeepReadonly 是一个递归的映射类型,它将对象 T 的所有属性(包括嵌套对象)变为 readonly。实现时,我们需要对每个属性 K in keyof T 应用 readonly 修饰符,并递归地对属性值类型应用 DeepReadonly,但仅限于值类型为对象(非函数)的情况。为了避免将函数也递归,通常用条件类型排除函数。对于数组类型,我们使用递归处理元素类型。
参考代码typescript type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends Function ? T[P] : T[P] extends object ? DeepReadonly<T[P]> : T[P]; }; // 示例用法 interface User { name: string; address: { city: string; zip: number; }; update: () => void; } type ReadonlyUser = DeepReadonly<User>; // 所有属性变为 readonly,包括嵌套的 address - 编写一个泛型函数 `filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[]`,基于类型谓词进行过滤。好回答应覆盖
- 使用类型谓词作为 predicate 返回类型。
- 返回类型应为 T[],但通过谓词过滤后类型更精确。
- 利用泛型来保留数组元素的具体类型。
查看范例答案
filterArray 函数接收一个数组和一个类型谓词,返回过滤后的数组。类型谓词 `(item: T) => item is T` 表示如果谓词返回 true,则 item 的类型被收窄为 T。但是在泛型上下文中,通常我们使用 `(item: unknown) => item is T` 来过滤未知类型,但这里要求 predicate 参数类型为 `(item: T) => item is T`,这其实没有收窄效果,因为 item 已经是 T 类型。更合理的做法是使用更宽泛的输入类型或使用联合类型。不过按照题目要求,我们实现一个简单的版本。
参考代码typescript function filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[] { const result: T[] = []; for (const item of arr) { if (predicate(item)) { result.push(item); } } return result; } // 示例:过滤字符串数组中的非空字符串 const arr = ['hello', '', 'world'] as (string | undefined)[]; const filtered = filterArray(arr, (item): item is string => typeof item === 'string' && item.length > 0); // filtered 类型为 string[] - `unknown` 类型是什么?它和 `any` 有什么不同?提供每个使用场景的例子。好回答应覆盖
- unknown 是 top type,而 any 则完全放弃类型检查。
- unknown 类型必须经过类型缩小才能使用,any 可以直接使用。
- unknown 更安全,适合用于未知数据(如 API 响应)。
- any 适合迁移旧代码或快速原型。
查看范例答案
unknown 和 any 都是通用类型,但 unknown 是类型安全的:任何值都可以赋给 unknown,但 unknown 类型的值不能直接使用(需要类型断言或缩小),这迫使开发者进行类型检查。而 any 会关闭所有类型检查,可以随意调用方法,容易引入运行时错误。因此,在编写新代码时优先使用 unknown 而不是 any。例如,接收一个 JSON 解析结果时,用 unknown 类型确保后续处理需要验证类型;而 any 则用于快速绕过类型检查,比如与旧的 JavaScript 库交互。
- 如何给一个返回用户对象 Promise 的异步函数添加类型?展示如何处理 resolved 和 rejected 状态。好回答应覆盖
- 为返回 Promise 的函数指定类型,使用泛型 Promise<T>。
- resolved 状态对应 Promise<T> 的 T。
- rejected 状态默认是 unknown(或 Error),可以通过 catch 处理。
- 可以使用 .then 和 .catch 或 async/await 处理。
查看范例答案
给返回用户对象 Promise 的异步函数添加类型,只需声明返回类型为 `Promise<User>`。其中 User 是用户对象的类型定义。例如:`async function getUser(id: string): Promise<User> { ... }`。在调用时,resolved 状态通过 await 得到 User 类型,rejected 状态会抛出异常,可以用 try/catch 捕获,但 catch 的参数默认是 unknown,需要缩小处理。
参考代码typescript interface User { id: string; name: string; } async function fetchUser(id: string): Promise<User> { const response = await fetch(`/users/${id}`); if (!response.ok) { throw new Error('Failed to fetch'); } return response.json() as Promise<User>; } // 调用 async function main() { try { const user: User = await fetchUser('123'); console.log(user.name); } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); } } } - 设计一个类型安全的事件发射器类,强制执行事件名称和负载类型。好回答应覆盖
- 使用泛型事件名称映射到负载类型。
- 定义 EventMap 类型约束。
- emit 方法需要事件名称和对应的负载。
- on 方法注册监听器,使用类型安全。
查看范例答案
设计类型安全的事件发射器,需要定义一个泛型 Events 类型,将事件名称映射到负载类型。类使用这个泛型来约束 emit 和 on 方法。例如,`interface Events { click: { x: number; y: number }; focus: void }`。emit 方法接受事件名称 K 和对应的负载 Events[K];on 方法接受事件名称 K 和回调函数,回调函数参数类型为 Events[K]。这样在编译时就能捕获错误:如果发送错误负载或监听不存在的名称,会报错。
参考代码typescript type Listener<T> = (payload: T) => void; class TypedEventEmitter<Events extends Record<string, unknown>> { private listeners: Map<keyof Events, Listener<unknown>[]> = new Map(); on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(listener as Listener<unknown>); } emit<K extends keyof Events>(event: K, payload: Events[K]): void { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.forEach(listener => (listener as Listener<Events[K]>)(payload)); } } } // 示例 interface MyEvents { userLogin: { userId: string; timestamp: number }; logout: void; } const emitter = new TypedEventEmitter<MyEvents>(); emitter.on('userLogin', (data) => { console.log(data.userId); // data 类型为 { userId: string; timestamp: number } }); emitter.emit('userLogin', { userId: '42', timestamp: Date.now() }); - 解释 `keyof` 和 `typeof` 操作符如何工作。提供使用它们创建动态类型的例子。好回答应覆盖
- keyof 返回对象类型的所有键的联合类型。
- typeof 用于获取变量或类型的类型(在类型上下文中)。
- 结合使用可以创建动态类型,如从已有对象提取键。
- 示例:使用 keyof 和 typeof 创建枚举映射。
查看范例答案
keyof 操作符用于获取对象类型的所有公共属性键的联合类型,例如 `keyof { a: number; b: string }` 得到 `'a' | 'b'`。typeof 在类型上下文中用于获取变量或表达式的类型,例如 `const x = { a: 1 }; type T = typeof x;` 得到 `{ a: number }`。两者结合可以创建动态类型,比如使用 `keyof typeof` 从一个常量对象生成键的联合类型,用于确保函数参数只能是该对象的键。
参考代码typescript const colors = { red: '#ff0000', green: '#00ff00', blue: '#0000ff', } as const; // 使用 keyof typeof 获取 'red' | 'green' | 'blue' type ColorName = keyof typeof colors; function getColor(name: ColorName): string { return colors[name]; } // 示例:动态创建类型映射 interface User { id: number; name: string; email: string; } type UserKeys = keyof User; // 'id' | 'name' | 'email' // 使用 typeof 获取函数返回类型 function createUser() { return { id: 1, name: 'Alice' }; } type CreatedUser = ReturnType<typeof createUser>; // { id: number; name: string } - 使用映射类型创建一个 `FlagProperties<T>` 类型,将对象类型 `T` 的所有布尔属性转换为字符串字面量 'true' | 'false'。好回答应覆盖
- 映射类型用于转换对象类型。
- 使用条件类型检查属性值是否为 boolean。
- 将 boolean 类型的属性值转为 'true' | 'false' 联合。
- 非 boolean 属性保持不变。
查看范例答案
FlagProperties<T> 是一个映射类型,它遍历 T 的所有属性,对于每个属性值类型为 boolean 的属性,将其值类型改为字面量联合类型 'true' | 'false';对于非 boolean 属性,保持原类型。这可以通过条件类型 `T[P] extends boolean ? 'true' | 'false' : T[P]` 实现。注意,这个转换不会修改属性名,只修改值类型。
参考代码typescript type FlagProperties<T> = { [P in keyof T]: T[P] extends boolean ? 'true' | 'false' : T[P]; }; // 示例 interface Config { enabled: boolean; name: string; visible: boolean; } type TransformedConfig = FlagProperties<Config>; // 结果: { enabled: 'true' | 'false'; name: string; visible: 'true' | 'false' }
如何准备
- 通过从头实现工具类型(例如 Partial、Pick、ReturnType)来练习。
- 理解结构类型(鸭子类型)及其对类型兼容性的影响。
- 掌握高级模式,如判别联合和品牌类型。
- 使用 TypeScript Playground 和官方挑战(例如 type challenges)来磨练技能。
- 审查真实的 TypeScript 代码库(例如 React 或 Node.js 库)以了解实际模式。
常见问题
对于 React 岗位,我需要了解 TypeScript 吗?
是的,因为大多数现代 React 代码库使用 TypeScript 来尽早捕获错误并改善开发者体验。
如何为面试练习 TypeScript?
使用 TypeScript Playground,在 GitHub 上解决类型挑战(例如 type-challenges),并实现小型项目。
TypeScript 面试中常见的错误是什么?
过度使用 `any`,忽略严格模式,以及不理解条件类型的工作原理。
TypeScript 难学吗?
基础容易,但高级特性如条件类型和模板字面量类型需要练习。
我应该记住类型定义吗?
专注于理解概念而非记忆。知道何时使用泛型或工具类型更重要。
练习 TypeScript 题目,即时获取 AI 反馈
上传简历,获得个性化模拟面试,并了解需要改进的地方——免费开始。