TypeScript 高频面试题
TypeScript 高频面试题
简介
TypeScript 是 JavaScript 的超集,通过添加静态类型系统提升了代码的可维护性和开发效率,已成为现代前端和 Node.js 开发的主流选择。本文整理了 TypeScript 面试中的高频问题,涵盖类型系统基础、泛型编程、工具类型、类型守卫、声明文件等核心知识点,帮助开发者全面准备 TypeScript 相关面试。
特点
类型系统基础
1. TypeScript 和 JavaScript 的区别是什么?为什么要使用 TypeScript?
TypeScript 是 JavaScript 的超集,主要区别在于:TypeScript 添加了静态类型注解,在编译期检查类型错误;支持接口(interface)、枚举(enum)、泛型(generics)等 JavaScript 没有的特性;支持最新的 ECMAScript 特性并可以编译到低版本 JavaScript;提供更好的 IDE 支持(智能提示、重构、跳转定义)。使用 TypeScript 的核心价值:在编译阶段发现大量潜在 Bug,减少运行时错误;类型即文档,提升代码可读性和可维护性;大型项目中团队协作更加高效;重构更加安全,类型系统会自动标识受影响的代码。
2. TypeScript 中 type 和 interface 的区别是什么?
两者在大多数场景下可以互换使用,但有以下区别:interface 支持声明合并(同名 interface 会自动合并属性),type 不支持;interface 使用 extends 实现继承,type 使用交叉类型 & 实现组合;type 可以定义联合类型、元组类型、基本类型别名等,interface 只能描述对象形状;type 可以使用 typeof 获取变量的类型,interface 不可以。选择建议:定义对象的形状、类的契约时优先使用 interface(可扩展性好);需要联合类型、交叉类型、映射类型等高级类型操作时使用 type。
3. TypeScript 中 any、unknown、never、void 的区别是什么?
any 表示任意类型,放弃类型检查,可以赋值给任何类型,是类型安全的逃生舱口。unknown 是类型安全的 any,可以接收任何值,但使用前必须进行类型检查(类型缩窄),不能直接赋值给其他类型。never 表示永远不会存在的类型,如永远不会返回的函数(抛出异常或无限循环),或在类型缩窄后不可能匹配的类型。void 表示函数没有返回值(返回 undefined)。unknown 是 any 的安全替代,never 常用于穷尽检查(exhaustive check),确保 switch/if 分支覆盖所有可能。
4. TypeScript 中枚举(enum)有哪些类型?
TypeScript 枚举分为四种:数字枚举(默认,值从 0 自增,支持反向映射);字符串枚举(每个成员必须显式赋字符串值,不支持反向映射);异构枚举(混合数字和字符串,不推荐使用);const 枚举(编译时内联,不会生成运行时代码)。此外还有 as const 断言可以创建类似枚举的效果。生产环境中建议:优先使用字符串枚举(可读性好,调试方便);不需要反向映射时使用 const 枚举减少代码体积;对于简单场景可以用联合字面量类型替代枚举。
5. TypeScript 中什么是类型推断?什么情况下需要显式注解?
类型推断是 TypeScript 编译器根据上下文自动推导变量类型的能力,无需手动标注。变量初始化时自动推断类型(let x = 42 推断为 number);函数返回值根据 return 语句推断;对象字面量的属性类型自动推断;解构赋值时保留原类型。需要显式注解的场景:函数参数(无法推断,必须标注);复杂的函数返回值类型(避免推断不准确);想要表达比推断更宽泛或更精确的类型;实现接口或类型别名时需要满足契约;使用 as 断言覆盖推断结果。
泛型相关
6. 什么是泛型?泛型的作用是什么?
泛型是一种参数化类型的机制,允许在定义函数、接口或类时使用类型占位符,在实际使用时才确定具体类型。泛型的核心作用:提高代码复用性,一套逻辑适用于多种类型;保证类型安全,在使用时才确定类型,编译期进行完整类型检查;避免使用 any 丢失类型信息。泛型使用 <T> 语法定义类型参数,支持多个类型参数(如 <T, U>),支持默认类型(如 <T = string>),支持约束泛型(<T extends SomeType>)。
7. 泛型约束(extends)如何使用?
泛型约束通过 extends 关键字限制类型参数必须满足特定条件。基本用法:<T extends Lengthwise> 要求 T 必须有 length 属性。在约束中使用类型参数:<T, U extends T> 要求 U 必须是 T 的子类型。使用 keyof 约束属性名:<T, K extends keyof T> 确保 K 是 T 的合法属性名。约束类型参数为构造函数:<T extends { new (...args: any[]): {} }>。泛型约束让泛型既有灵活性又有安全性,编译器可以确保类型参数具备所需的属性或方法。
8. 如何实现泛型工具函数 deepReadonly 和 deepPartial?
deepReadonly 将对象所有层级的属性变为只读:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
// 使用示例
interface Config {
name: string;
database: { host: string; port: number };
}
type ReadonlyConfig = DeepReadonly<Config>;
// 所有层级属性均为 readonlydeepPartial 将对象所有层级属性变为可选:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepPartial<T[K]>
: T[K];
};这两个工具类型通过递归类型引用实现深层类型的转换,是 TypeScript 高级类型编程的经典应用。
工具类型相关
9. TypeScript 内置了哪些常用工具类型(Utility Types)?
常用工具类型包括:Partial<T> 将所有属性变为可选;Required<T> 将所有属性变为必选;Readonly<T> 将所有属性变为只读;Record<K, V> 构建键类型为 K、值类型为 V 的对象类型;Pick<T, K> 从 T 中选取部分属性;Omit<T, K> 从 T 中排除部分属性;Exclude<U, E> 从联合类型 U 中排除 E;Extract<U, E> 从联合类型 U 中提取 E;NonNullable<T> 排除 null 和 undefined;ReturnType<T> 获取函数返回值类型;Parameters<T> 获取函数参数类型的元组;InstanceType<T> 获取构造函数的实例类型。
10. 如何实现一个自定义的条件类型工具?
条件类型(Conditional Types)使用 T extends U ? X : Y 语法,类似于类型层面的三元表达式。自定义工具类型示例:
// 判断类型是否为字符串
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// 将对象类型的值类型进行转换
type Stringify<T> = {
[K in keyof T]: string;
};
// 提取 Promise 内部的值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<string>>; // string
// 实现简易版本的 Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};infer 关键字用于在条件类型中推断类型变量,常用于提取嵌套类型中的内部类型。
11. 什么是映射类型(Mapped Types)?
映射类型是一种通过变换已有类型的属性来创建新类型的方式,语法为 { [K in keyof T]: NewType }。映射类型可以对属性名进行重映射(TS 4.1+):{ [K in keyof T as NewKey]: T[K] }。常见应用:将所有属性变为可选(Partial)、只读(Readonly);根据条件过滤属性(如移除函数属性);修改属性名的命名风格(如驼峰转下划线);创建标签联合类型(Discriminated Union)的映射。映射类型结合模板字面量类型(Template Literal Types)可以实现更强大的类型变换。
类型守卫相关
12. 什么是类型守卫(Type Guards)?有哪些方式?
类型守卫是一种在运行时检查类型并在编译时缩窄类型范围的机制。常见的类型守卫方式:typeof 守卫(检查基本类型 string、number、boolean);instanceof 守卫(检查类实例);in 操作符(检查对象是否有某属性);自定义类型谓词(User-Defined Type Guards,使用 parameterName is Type 返回类型);switch / if 字面量类型检查(检查字符串字面量或联合类型的分支)。类型守卫之后,TypeScript 编译器会在对应分支内缩窄变量的类型,提供精确的类型提示。
13. 如何编写自定义类型守卫?
自定义类型守卫使用 parameterName is Type 作为函数返回类型:
// 基本的自定义类型守卫
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(); // TypeScript 知道 pet 是 Fish
} else {
pet.fly(); // TypeScript 知道 pet 是 Bird
}
}
// 更复杂的类型守卫:检查接口形状
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
function isApiResponse<T>(value: unknown, isData: (v: unknown) => v is T): value is ApiResponse<T> {
return typeof value === 'object' && value !== null
&& 'code' in value && 'data' in value && 'message' in value
&& typeof (value as ApiResponse<T>).code === 'number'
&& isData((value as ApiResponse<T>).data);
}14. 什么是可辨识联合(Discriminated Union)?
可辨识联合是 TypeScript 中一种常用的类型设计模式,通过在联合类型的每个成员中添加一个相同名称但不同字面量值的属性(标签字段),配合类型守卫实现类型安全。典型应用场景:状态管理(如 Redux Action)、异步请求状态、AST 节点类型等。编译器会根据标签字段的值自动缩窄联合类型到对应的分支。
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rectangle': return shape.width * shape.height;
case 'triangle': return 0.5 * shape.base * shape.height;
}
}
// 穷尽检查:如果新增类型但未处理,编译器会报错
function assertNever(x: never): never {
throw new Error('Unexpected object: ' + x);
}声明文件相关
15. 什么是声明文件(.d.ts)?如何使用?
声明文件以 .d.ts 为扩展名,仅包含类型声明,不包含运行时代码,用于描述 JavaScript 代码的类型信息。声明文件的主要用途:为没有 TypeScript 类型的第三方 JavaScript 库提供类型定义;在项目中将类型声明与实现分离;发布 npm 包时同时提供类型声明。大多数流行的 JavaScript 库的类型定义可以在 DefinitelyTyped 仓库(@types/package-name)中找到。安装方式:npm install @types/lodash --save-dev。如果找不到现成的类型声明,可以自己编写声明文件并配置 tsconfig.json 的 typeRoots 或 paths。
16. tsconfig.json 中有哪些关键配置项?
tsconfig.json 的关键配置项:strict(启用所有严格类型检查选项,推荐开启);target(编译输出的 JavaScript 版本,如 ES2020);module(模块系统,如 ESNext、CommonJS);moduleResolution(模块解析策略,推荐 node);esModuleInterop(允许 default 导入互操作);skipLibCheck(跳过声明文件类型检查,加快编译速度);baseUrl 和 paths(模块路径别名映射);include 和 exclude(指定编译包含和排除的文件);declaration(生成 .d.ts 声明文件);outDir(输出目录);sourceMap(生成 source map)。
综合进阶
17. TypeScript 中协变和逆变是什么?
协变(Covariance)和逆变(Contravariance)是描述类型兼容性方向的概念。协变:如果 Dog extends Animal,则 Array<Dog> 也是 Array<Animal> 的子类型(类型方向一致),适用于只读/输出位置。逆变:如果 Dog extends Animal,则 (arg: Animal) => void 是 (arg: Dog) => void 的子类型(类型方向相反),适用于只写/输入位置。TypeScript 中函数参数默认是双变的(既协变又逆变,strictFunctionTypes 关闭时),开启 strictFunctionTypes 后函数参数变为逆变,提供更严格的类型安全。
18. 如何在 TypeScript 中实现类型安全的事件系统?
// 定义事件映射接口
interface EventMap {
login: { userId: string; timestamp: Date };
logout: { userId: string };
error: { code: number; message: string };
}
// 类型安全的事件发射器
class TypeSafeEmitter<Events extends Record<string, any>> {
private listeners: { [K in keyof Events]?: Array<(data: Events[K]) => void> } = {};
on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners[event]?.forEach(listener => listener(data));
}
off<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
this.listeners[event] = this.listeners[event]?.filter(l => l !== listener);
}
}
// 使用时事件名和数据类型完全匹配
const emitter = new TypeSafeEmitter<EventMap>();
emitter.on('login', (data) => {
console.log(data.userId, data.timestamp); // 完整类型提示
});
emitter.emit('error', { code: 404, message: 'Not Found' }); // 类型安全19. TypeScript 中如何处理 this 类型?
TypeScript 允许在函数参数中声明 this 的类型,用于确保函数在正确的上下文中调用。在方法中返回 this 可以实现流畅接口(Fluent API)模式。在类中使用 this 类型可以让继承类的方法返回类型自动推断为子类类型。
class Calculator {
protected value = 0;
add(n: number): this {
this.value += n;
return this;
}
multiply(n: number): this {
this.value *= n;
return this;
}
getResult(): number {
return this.value;
}
}
// 链式调用
const result = new Calculator().add(5).multiply(3).getResult(); // 1520. TypeScript 项目中有哪些常见的类型错误?如何避免?
常见类型错误及避免方法:隐式 any(开启 noImplicitAny,确保所有参数和变量都有类型);类型断言滥用(优先使用类型守卫而非 as 断言);对象展开类型不完整(使用 & 交叉类型或工具类型确保属性完整);泛型过度使用(简单场景不需要泛型,保持代码可读性);忽略 null/undefined(开启 strictNullChecks,使用可选链和空值合并);循环引用类型(使用接口而非 type 别名,或使用 import type)。建议在项目初期就配置好严格的 tsconfig.json,配合 ESLint 的 TypeScript 规则,从源头上减少类型问题。
优点
缺点
总结
TypeScript 面试题涵盖了类型系统基础、泛型编程、工具类型、类型守卫和声明文件等核心知识领域。掌握这些知识点不仅有助于通过面试,更能在实际项目中编写出类型安全、可维护性高的代码。建议在学习过程中多阅读 TypeScript 标准库的类型定义源码,实践高级类型编程技巧,逐步提升类型系统的运用能力。
21. TypeScript 中 Module 和 Namespace 的区别?
Module(ES Module)是 TypeScript 推荐的代码组织方式,使用 import/export 语法,每个文件就是一个模块。Module 有自己的作用域,不会污染全局。Module 是运行时概念,会被编译为 CommonJS 或 ES Module。
Namespace 是 TypeScript 早期(1.x 时代)的代码组织方式,使用 namespace 关键字。Namespace 主要用于将内部模块(internal modules)组织到全局命名空间中。Namespace 不推荐在新项目中使用,主要保留用于兼容旧代码和声明文件中的全局类型组织(如 declare namespace jQuery)。
选择建议:新项目一律使用 ES Module,避免使用 Namespace。Namespace 仅在声明全局变量类型或扩展现有库的类型时使用。
22. TypeScript 中如何实现函数重载?
TypeScript 的函数重载是通过提供多个函数签名(overload signatures)和一个实现签名来实现的。编译器会根据调用时的参数类型匹配对应的重载签名。
// 函数重载签名
function formatDate(date: Date): string;
function formatDate(timestamp: number): string;
function formatDate(dateStr: string, format: string): string;
// 实现签名(必须兼容所有重载签名)
function formatDate(dateOrTimestamp: Date | number | string, format?: string): string {
if (typeof dateOrTimestamp === 'number') {
return new Date(dateOrTimestamp).toISOString();
}
if (typeof dateOrTimestamp === 'string') {
return dayjs(dateOrTimestamp).format(format || 'YYYY-MM-DD');
}
return dateOrTimestamp.toISOString();
}
// 调用时 TypeScript 会根据参数类型匹配正确的签名
formatDate(new Date()); // 匹配第一个签名
formatDate(Date.now()); // 匹配第二个签名
formatDate('2024-01-01', 'DD/MM/YYYY'); // 匹配第三个签名注意:实现签名对外部不可见,调用者只能看到重载签名。实现签名的参数类型必须是所有重载签名的超集。
23. TypeScript 中 as const 和 readonly 的区别?
as const 是 TypeScript 3.4 引入的断言,将值类型收窄为最窄的字面量类型。应用对象时,所有属性变为 readonly,数组变为 readonly tuple。
readonly 是类型修饰符,标记属性为只读,在类型层面禁止赋值操作。两者可以结合使用。
// as const:值层面的断言
const config = { host: 'localhost', port: 3000 } as const;
// 类型为 { readonly host: 'localhost', readonly port: 3000 }
// config.host = '192.168.1.1'; // 编译错误
// readonly:类型层面的修饰
interface Config {
readonly host: string;
readonly port: number;
}
const config2: Config = { host: 'localhost', port: 3000 };
// config2.host = '192.168.1.1'; // 编译错误
// 关键区别:as const 保留字面量类型,readonly 不影响值的类型收窄
const a = { x: 42 } as const; // a.x 类型为 42(字面量)
const b: { readonly x: number } = { x: 42 }; // b.x 类型为 number24. 什么是声明合并(Declaration Merging)?
声明合并是指 TypeScript 编译器将多个同名的声明合并为一个声明的能力。主要用于 interface 的扩展。
// Interface 声明合并
interface Window {
myCustomProperty: string;
}
interface Window {
myOtherProperty: number;
}
// 合并后等价于:
// interface Window {
// myCustomProperty: string;
// myOtherProperty: number;
// }
// Namespace 的声明合并
namespace MyLib {
export function fn1() {}
}
namespace MyLib {
export function fn2() {}
}
// 合并后包含 fn1 和 fn2
// Class 与 Namespace 的合并(为类添加静态成员)
class Utils {}
namespace Utils {
export function helper() {}
}
// 注意:type 别名不支持声明合并,这是 interface 和 type 的核心区别之一25. TypeScript 中 satisfies 运算符的作用?
satisfies 是 TypeScript 4.9 引入的运算符,用于验证表达式的类型是否符合某个类型,同时不改变表达式的推断类型。
// 问题:使用类型注解会丢失具体类型
const colors = { red: '#ff0000', green: '#00ff00', blue: '#0000ff' } as Record<string, string>;
// colors.red 的类型是 string,不是 '#ff0000'
// 使用 satisfies 保留具体类型
const colors2 = { red: '#ff0000', green: '#00ff00', blue: '#0000ff' } satisfies Record<string, string>;
// colors2.red 的类型是 '#ff0000',保留了字面量类型
// 同时确保所有值都是 string 类型
// 实际应用场景:验证配置对象的结构
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
} satisfies {
apiUrl: string;
timeout: number;
retries: number;
};
// config.timeout 的类型是 5000(字面量),而不是 number26. TypeScript 中如何实现类型安全的 EventEmitter?
// 类型安全的事件发射器
class TypedEventEmitter<TEventMap extends Record<string, unknown>> {
private listeners = new Map<keyof TEventMap, Set<Function>>();
on<K extends keyof TEventMap>(
event: K,
listener: (payload: TEventMap[K]) => void
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
return () => this.off(event, listener); // 返回取消订阅函数
}
off<K extends keyof TEventMap>(
event: K,
listener: (payload: TEventMap[K]) => void
): void {
this.listeners.get(event)?.delete(listener);
}
emit<K extends keyof TEventMap>(event: K, payload: TEventMap[K]): void {
this.listeners.get(event)?.forEach(listener => listener(payload));
}
}
// 使用示例
interface AppEvents {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'notification': { message: string; level: 'info' | 'warn' | 'error' };
}
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on('user:login', (payload) => {
console.log(payload.userId, payload.timestamp); // 完整类型提示
});
// emitter.on('user:logn', ...); // 编译错误:事件名拼写错误27. TypeScript 中 infer 关键字的高级用法?
// infer 用于在条件类型中推断嵌套类型
// 1. 提取函数返回值类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Result = MyReturnType<() => string>; // string
type Result2 = MyReturnType<(x: number) => boolean>; // boolean
// 2. 提取数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Item = ElementOf<string[]>; // string
type Item2 = ElementOf<number[]>; // number
// 3. 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type AsyncResult = UnwrapPromise<Promise<Promise<string>>>; // string
// 4. 提取构造函数实例类型
type InstanceOf<T> = T extends new (...args: any[]) => infer U ? U : never;
type UserInstance = InstanceOf<typeof User>; // User
// 5. 递归提取函数参数类型(反向的 Parameters)
// TypeScript 内置的 Parameters<T> 就是这么实现的
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = MyParameters<(name: string, age: number) => void>;
// [name: string, age: number]
// 6. 提取对象中满足条件的属性
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K]
};
interface Config { name: string; age: number; active: boolean }
type StringFields = PickByValue<Config, string>; // { name: string }这组题真正考什么
- 面试官通常想知道你是否真正理解浏览器、框架和工程化之间的联系。
- 高频追问往往从概念定义延伸到性能、兼容性和线上诊断。
- 如果能结合真实页面问题回答,可信度会明显提高。
60 秒答题模板
- 先说这个概念解决什么问题。
- 再说它在浏览器或框架里的工作机制。
- 最后补一个线上场景或优化案例。
容易失分的点
- 只背 API 名称,不理解执行链路。
- 只说框架,不说浏览器原理。
- 回答性能题时没有指标和验证手段。
刷题建议
- 把浏览器、框架、工程化和性能题分开复习,避免知识点混在一起。
- 每道题尽量补一个页面真实案例,比如登录流程、首屏优化或状态同步。
- 前端题常考对比题,复习时要准备两到三个维度的横向比较。
高频追问
- 这个概念在 React、Vue、原生浏览器里分别怎么体现?
- 如果线上出现白屏、性能抖动或状态错乱,你会怎么定位?
- 这个方案的可维护性和性能代价是什么?
复习重点
- 把每道题的关键词整理成自己的知识树,而不是只背原句。
- 对容易混淆的概念要做横向比较,例如机制差异、适用边界和性能代价。
- 复习时优先补“为什么”,其次才是“怎么用”和“记住什么术语”。
面试作答提醒
- 先说结论与应用场景,再解释机制。
- 讲性能题时尽量带上监控指标。
- 框架题要注意区分版本特性和通用原理。
