1. 项目概述:一份面向实战的TypeScript面试深度解析
最近几年,前端技术栈的深度和广度都在飞速扩展,TypeScript 从一个可选的“甜点”变成了许多中大型项目的“标配”。随之而来的,是面试中对 TypeScript 考察的权重越来越高。很多朋友,尤其是从 JavaScript 转过来的开发者,面对 TypeScript 面试题时,常常感觉“概念都懂,但一深问就懵”,或者“知道有泛型、装饰器,但被问到实际应用场景和原理就卡壳”。
我经历过不少面试,也作为面试官考察过很多人,发现一个普遍现象:大家刷题时往往只记住了“答案”,比如any和unknown的区别是“any会绕过类型检查,unknown更安全”,但被追问“为什么安全?在什么场景下必须用unknown?如何从unknown安全地缩小类型范围?”时,回答就变得模糊了。这份“超详细”的解析,目的就是帮你穿透这些表面答案,深入到 TypeScript 的类型系统、设计模式、工程实践和编译原理层面,让你不仅能答对题,更能讲出背后的“所以然”,在面试中展现出真正的理解深度和工程能力。
这份指南适合所有准备前端或全栈开发面试的开发者,无论你是刚接触 TypeScript 不久,希望系统梳理核心概念的新手,还是有一定经验,想查漏补缺、深化理解的资深工程师。我们将从最基础的静态类型优势谈起,逐步深入到高级类型工具、泛型编程、装饰器、模块与命名空间,最后探讨工程配置和性能优化,覆盖面试中超过 95% 的高频考点和难点。
2. 核心概念与类型系统深度剖析
2.1 静态类型的价值远不止“找错”
面试官常问:“为什么用 TypeScript?和 JavaScript 比优势在哪?” 标准答案是“静态类型检查,提前发现错误,增强代码可维护性”。但这个答案太浅了。我们需要从几个维度展开:
第一,它是设计阶段的契约和文档。当你定义一个函数function fetchUser(id: number): Promise<User>时,你不仅仅是在声明类型,你是在为这个函数的使用者(可能是未来的你,也可能是你的同事)签订一份清晰的契约:我需要一个number类型的id,我会返回一个包含User对象的Promise。这份契约在代码编写时就被 IDE 理解和强制执行,远比写在注释里或口头约定可靠。它极大地降低了模块间的沟通成本和集成风险。
第二,它赋能现代 IDE 的智能体验。基于类型信息,VS Code 等编辑器能提供精准的代码补全、参数提示、跳转到定义、查找所有引用、智能重构(如重命名符号)等功能。这不仅仅是提升编码速度,更是减少认知负担,让你能更专注于业务逻辑,而不是记忆 API 细节。
第三,它重构的“安全网”。在大型项目中,重构是常态。如果你想把一个接口的某个属性从string改为number,在纯 JavaScript 中,你需要手动全局搜索、小心翼翼地修改,并祈祷没有遗漏。在 TypeScript 中,你只需修改类型定义,然后编译(或启动类型检查),所有类型不匹配的地方都会立即被标红。这让你有信心进行大规模重构,而不用担心引入难以察觉的运行时错误。
注意:很多初学者会过度使用
any类型来快速绕过类型错误,这相当于主动放弃了 TypeScript 的核心优势。正确的做法是,即使一时无法确定精确类型,也应优先使用unknown或定义更宽泛但仍有约束的联合类型、接口。
2.2 类型基础:从any,unknown,never说起
这是几乎必考的基础题,但区分它们需要理解类型系统的设计哲学。
any:类型系统的“逃生舱”。它告诉编译器:“别管我,我知道我在做什么”。任何值都可以赋值给any类型的变量,你也可以对它进行任何操作(访问属性、调用方法、算术运算),编译器都不会报错。这听起来很自由,但极其危险,因为它将类型安全的责任完全转移给了开发者,很容易引入运行时错误。它的存在主要是为了兼容旧的 JavaScript 代码或处理确实无法预知类型的动态内容(但应尽快将其收敛到更具体的类型)。
unknown:类型安全的“顶级类型”。TypeScript 3.0 引入。任何值也可以赋值给unknown类型的变量,这是它和any的相似之处。但关键区别在于:你不能对unknown类型的变量进行任意操作。在对其进行任何操作(除了比较操作如===,==,!==,!=)之前,你必须通过类型断言、类型守卫或类型收窄来告诉编译器它到底是什么类型。
let value: unknown = “hello”; // 错误:Object is of type ‘unknown’. // console.log(value.toUpperCase()); // 正确:使用类型断言 console.log((value as string).toUpperCase()); // 正确:使用类型守卫 if (typeof value === ‘string’) { console.log(value.toUpperCase()); // 在此分支内,value 被收窄为 string }never:表示“永不存在的值”的“底部类型”。它是所有类型的子类型。never类型通常出现在以下场景:
- 抛出错误的函数:
function error(message: string): never { throw new Error(message); } - 无限循环的函数:
function infiniteLoop(): never { while (true) {} } - 类型收窄中排除所有可能性的分支:
type Shape = { kind: ‘circle’; radius: number } | { kind: ‘square’; side: number }; function getArea(shape: Shape): number { switch (shape.kind) { case ‘circle’: return Math.PI * shape.radius ** 2; case ‘square’: return shape.side ** 2; default: // 此时 shape 的类型被收窄为 never,因为所有可能性都已处理 const _exhaustiveCheck: never = shape; return _exhaustiveCheck; } }在default分支中,shape应该是never类型。如果你后续为Shape联合类型新增了一个成员(如{ kind: ‘triangle’; base: number; height: number }),但没有在switch中处理,那么shape在default分支中将不会是never,会导致const _exhaustiveCheck: never = shape;这一行报错,从而提醒你还有未处理的 case。这是一个利用类型系统实现“穷尽性检查”的经典技巧。
2.3 接口与类型别名的微妙差异与选用策略
interface和type都可以用来定义对象形状,面试中常被问及区别。
核心区别:
扩展方式:
interface使用extends进行继承,type使用&(交叉类型)进行组合。interface Animal { name: string; } interface Bear extends Animal { honey: boolean; } type Animal = { name: string; }; type Bear = Animal & { honey: boolean; };声明合并:
interface支持声明合并,即同一个名称的多个interface声明会自动合并。type不允许重复声明。interface User { name: string; } interface User { age: number; } // 最终 User 接口为 { name: string; age: number; } type User = { name: string; }; // 错误:重复标识符 ‘User’ type User = { age: number; };声明合并常用于为第三方库(如
window对象)或全局模块添加自定义属性。能力范围:
type能表达的范围更广。它可以定义联合类型、元组类型、映射类型、条件类型等,而interface主要描述对象结构。// type 可以,interface 不行 type ID = string | number; type Point = [number, number]; type Nullable<T> = T | null;
选用策略(个人经验):
- 优先使用
interface:当你需要定义一个对象的形状,并且预期它可能会被扩展(通过extends)或需要利用声明合并特性时。这在开发库或定义公共 API 时尤其有用,因为它提供了更好的扩展性。 - 使用
type:当你需要定义联合类型、元组、映射类型或更复杂的类型转换时。或者,当你定义的类型不会再被扩展,且你更喜欢使用&进行组合时。 - 在大多数定义对象形状的场景下,两者可以互换,团队保持风格一致即可。但理解其差异有助于在特定场景下做出更合适的选择。
3. 高级类型工具与泛型编程实战
3.1 泛型:编写灵活且可复用的类型代码
泛型是 TypeScript 的支柱,它允许你创建可重用的组件,这些组件可以支持多种类型,而不是单一类型。理解泛型的关键在于将其视为“类型的函数参数”。
基础泛型函数与接口:
// 一个简单的身份函数,不使用泛型时,它要么只处理特定类型,要么使用 any function identity(arg: any): any { return arg; } // 丢失了类型信息 // 使用泛型 function identity<T>(arg: T): T { return arg; } // 调用时,T 会被自动推断为传入参数的类型 let output = identity<string>(“myString”); // 显式指定 T 为 string let output2 = identity(“myString”); // 更常见:类型推断,T 被推断为 string泛型约束:有时你需要限制泛型参数必须符合某种形状,使用extends关键字。
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // 现在我们知道 arg 一定有 .length 属性 return arg; } loggingIdentity(3); // 错误:number 没有 .length 属性 loggingIdentity({length: 10, value: 3}); // 正确泛型在 React 组件中的典型应用:
interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; } function List<T>({ items, renderItem }: ListProps<T>) { return <div>{items.map(renderItem)}</div>; } // 使用 <List<{id: number, name: string}> items={users} renderItem={(user) => <div key={user.id}>{user.name}</div>} />这样,List组件可以渲染任何类型的数组,同时保持item在renderItem函数内的类型安全。
3.2 实用工具类型:TypeScript 提供的“类型工具箱”
TypeScript 内置了许多工具类型,用于常见的类型转换。理解它们能极大提升类型定义的效率。
Partial<T>:将类型T的所有属性设置为可选。
interface User { name: string; age: number; } type PartialUser = Partial<User>; // { name?: string; age?: number; }常用于更新对象的场景,比如function updateUser(id: string, updates: Partial<User>)。
Required<T>:与Partial相反,将所有属性变为必选。
Readonly<T>:将类型T的所有属性设置为只读。
Pick<T, K>:从类型T中挑选出一组属性K来构造新类型。
type UserPreview = Pick<User, ‘name’ | ‘id’>; // { name: string; id: number }Omit<T, K>:从类型T中排除一组属性K来构造新类型。
type UserWithoutId = Omit<User, ‘id’>; // { name: string; age: number }Record<K, T>:构造一个对象类型,其属性键为K类型,属性值为T类型。
type PageInfo = Record<‘home’ | ‘about’ | ‘contact’, { title: string }>; // 等价于 { home: { title: string }; about: { title: string }; contact: { title: string }; }ReturnType<T>:获取函数类型T的返回值类型。
type Fn = () => { x: number; y: string }; type T = ReturnType<Fn>; // { x: number; y: string }Parameters<T>:获取函数类型T的参数类型元组。
实操心得:在定义组件 Props 或函数参数时,善用
Pick,Omit,Partial可以避免重复定义大量相似的类型,使代码更 DRY(Don‘t Repeat Yourself)。例如,一个表单编辑组件可能接受Omit<User, ‘id’>作为初始值,而详情展示组件接受完整的User。
3.3 条件类型与 infer 关键字:类型层面的逻辑编程
条件类型是 TypeScript 2.8 引入的强大特性,形式为T extends U ? X : Y。它允许根据类型关系进行选择。结合infer关键字,可以在条件类型中声明一个待推断的类型变量,实现复杂的类型提取和转换。
一个经典例子:Flatten类型(展平数组):
type Flatten<T> = T extends Array<infer U> ? U : T; // 使用 type Str = Flatten<string[]>; // string type Num = Flatten<number>; // number这里,T extends Array<infer U>是一个条件判断。如果T是某种类型的数组,则infer U会捕获这个元素的类型,并返回U;否则,直接返回T。
更复杂的例子:提取函数返回的 Promise 内部类型:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; type A = UnwrapPromise<Promise<string>>; // string type B = UnwrapPromise<number>; // number内置工具类型ReturnType和Parameters就是利用条件类型和infer实现的:
// ReturnType 的简化原理 type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;条件类型是构建高级、灵活类型工具的基础,在编写通用库或框架时尤其有用。虽然日常业务开发中直接编写复杂条件类型的机会不多,但理解其原理对于读懂社区优秀库的类型定义至关重要。
4. 装饰器、模块与工程化配置
4.1 装饰器:元编程与增强逻辑的利器
装饰器是一种特殊类型的声明,它可以被附加到类声明、方法、访问器、属性或参数上。装饰器使用@expression的形式,expression求值后必须为一个函数,它会在运行时被调用,并以被装饰的声明信息作为参数。注意:装饰器在 TypeScript 中仍是一个实验性特性,需要在tsconfig.json中启用“experimentalDecorators”: true。
类装饰器:应用于类构造函数,可以用来观察、修改或替换类定义。
function LogClass(target: Function) { console.log(`类 ${target.name} 被装饰。`); } @LogClass class MyClass { constructor() {} } // 输出:类 MyClass 被装饰。方法装饰器:应用于方法描述符,可以用于日志、计时、权限校验等。
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`调用方法: ${propertyKey},参数:`, args); const result = originalMethod.apply(this, args); console.log(`方法 ${propertyKey} 返回:`, result); return result; }; } class Calculator { @LogMethod add(x: number, y: number): number { return x + y; } }属性装饰器、参数装饰器原理类似。装饰器在 Angular、NestJS 等框架中广泛应用,用于定义组件、模块、依赖注入等。理解装饰器有助于你理解这些框架的工作原理。
注意事项:装饰器的执行顺序有明确规定(例如,参数装饰器先于方法装饰器,同类多个装饰器从下到上执行等),在编写复杂装饰器时需要留意。此外,装饰器代码在编译时注入,可能会增加代码体积和复杂度,在业务代码中应谨慎使用,避免过度设计。
4.2 模块解析与命名空间:代码组织的两种范式
模块(ES Modules)是现代 JavaScript/TypeScript 项目组织代码的标准方式。使用import/export语法。TypeScript 支持查找.ts,.tsx,.d.ts文件,也支持通过moduleResolution配置(如“node”)来模拟 Node.js 的模块解析策略,查找node_modules中的包。
命名空间(Namespaces)是 TypeScript 早期用于组织代码、避免全局污染的方式,使用namespace关键字定义,内部成员通过export暴露。在同一个命名空间内的代码,即使分布在多个文件,也会被视为一个整体。但命名空间在现代前端工程中已逐渐被 ES 模块取代,因为模块是语言标准,且能被打包工具(如 Webpack, Rollup)更好地进行 Tree Shaking。
如何选择?
- 几乎总是使用 ES 模块。它是标准,具有更好的工具链支持和静态分析能力。
- 命名空间可能在一些遗留代码库中见到,或者在为浏览器环境编写不经过打包的、需要避免全局冲突的库时,仍有其用武之地。但在新项目中,应优先使用模块。
4.3 tsconfig.json 核心配置项解读
tsconfig.json是 TypeScript 项目的核心配置文件,面试官可能会问你一些关键配置项的含义。
compilerOptions下一些高频且重要的配置:
target: 指定编译后的 JavaScript 目标版本,如“es5”,“es2015”,“es2020”。需根据项目需要支持的浏览器或 Node.js 版本选择。module: 指定生成代码的模块系统,如“commonjs”(Node.js),“es2015”,“esnext”。需与你的运行环境和打包工具匹配。lib: 指定编译过程中需要包含的库文件定义,如[“es2015”, “dom”]。如果你在浏览器环境中使用fetchAPI,就需要包含“dom”。strict: 是否启用所有严格类型检查选项。强烈建议设置为true,这是保证代码质量的最重要开关。它包含了noImplicitAny,strictNullChecks,strictFunctionTypes等一系列选项。strictNullChecks: 当为true时,null和undefined只能赋值给它们自身和any类型。这能有效避免“未定义不是对象”这类经典错误。
moduleResolution: 模块解析策略,常用“node”,它模拟 Node.js 的解析方式。baseUrl/paths: 用于配置路径别名,简化模块导入。
然后可以这样导入:{ “compilerOptions”: { “baseUrl”: “./src”, “paths”: { “@components/*”: [“components/*”], “@utils/*”: [“utils/*”] } } }import Button from ‘@components/Button’;esModuleInterop: 为了兼容 CommonJS 模块(module.exports = …)的默认导入,通常设置为true。允许你使用import React from ‘react’;而不是import * as React from ‘react’;。skipLibCheck: 跳过对声明文件(.d.ts)的类型检查,可以加快编译速度,但可能会错过一些库本身的类型错误。对于大型项目,开启它可以显著提升编译性能。outDir: 指定编译输出目录。rootDir: 指定源文件的根目录,编译器会根据这个目录来组织输出目录的结构。
理解这些配置项,不仅能帮你正确搭建项目,也能在面试中体现出你对 TypeScript 工程化实践的了解。
5. 性能优化、错误排查与进阶实践
5.1 类型声明文件(.d.ts)与 DefinitelyTyped
当使用纯 JavaScript 编写的第三方库时,我们需要类型声明文件来获得 TypeScript 的类型支持。声明文件以.d.ts为后缀,里面只包含类型声明,没有具体的实现。
声明文件的来源:
- 库自带:越来越多的库在 npm 包中直接包含了类型声明(在
package.json中通过“types”或“typings”字段指定)。 - DefinitelyTyped:一个庞大的社区仓库,为没有自带类型的 JavaScript 库提供高质量的 TypeScript 类型定义。通过
@types/前缀的包安装,例如npm install --save-dev @types/lodash。 - 自己编写:如果使用的库既没有自带类型,也没有
@types/包,你就需要自己为它编写声明文件。通常放在项目根目录的typings文件夹或一个全局的.d.ts文件中。
编写简单的声明文件:
// global.d.ts 或 somelib.d.ts declare module ‘some-untyped-lib’ { export function doSomething(config: any): void; export const version: string; }declare关键字告诉 TypeScript 编译器,这个变量、函数或模块的类型已经在其他地方定义了,编译器不需要去实现它,只需信任这个类型声明。
5.2 常见编译错误与性能问题排查
1. 类型“X”上不存在属性“Y”这是最常见的错误之一。原因可能是:
- 变量类型推断错误,实际不是你以为的对象。使用类型断言或修改代码逻辑确保类型正确。
- 访问了可能为
undefined或null的属性。需要使用可选链?.或非空断言!(慎用)或进行判空。// 可选链 const name = user?.profile?.name; // 非空断言(你确信它不为空时) const name = user!.profile!.name;
2. 不能将类型“A”分配给类型“B”通常发生在函数参数、赋值或泛型约束不匹配时。需要仔细检查源类型和目标类型的结构是否兼容(结构化类型系统)。有时需要修改接口定义或使用类型断言。
3. 编译速度慢大型 TypeScript 项目编译可能很慢。优化策略:
- 启用
incremental: 增量编译,只重新编译更改的文件及其依赖。 - 启用
skipLibCheck: 如上所述。 - 使用
tsc --build(复合项目): 将大项目拆分为多个子项目,只编译更改的部分。 - 配置
exclude: 在tsconfig.json中排除node_modules和输出目录(如dist,build)。 - 考虑使用
ts-loader的transpileOnly模式:在 Webpack 中,如果不需要类型检查(可以交给 IDE 或单独的命令),可以开启此模式大幅提升编译速度,类型检查通过fork-ts-checker-webpack-plugin在另一个进程进行。
4. 项目引用(Project References)对于大型单体仓库(Monorepo),可以使用tsconfig.json中的references字段来建立项目间的依赖关系。这允许 TypeScript 理解项目结构,实现智能的增量构建和类型检查,避免重复编译依赖项。
5.3 在 React 与 Vue 3 中的最佳实践
React with TypeScript:
- 组件 Props 类型定义:使用
interface或type明确定义 Props。
注意:interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; } const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => { … };React.FC泛型类型隐式包含了children属性。如果你不希望组件接受children,可以直接标注函数返回值:const Button = (props: ButtonProps): JSX.Element => { … };。 - Hooks 类型:
useState,useReducer等 Hook 能很好地从初始值或 reducer 函数中推断类型。对于复杂状态,可以显式指定泛型参数:const [user, setUser] = useState<User | null>(null);。 - 事件类型:使用 React 提供的类型,如
React.MouseEvent<HTMLButtonElement>。
Vue 3 with TypeScript (Composition API):
- 使用
<script setup lang=“ts”>:这是最简洁的方式,能获得完美的类型推断。 - 定义 Props 和 Emits:使用
defineProps和defineEmits宏,它们支持基于类型的声明。<script setup lang=“ts”> interface Props { msg: string; } const props = defineProps<Props>(); const emit = defineEmits<{ (e: ‘change’, id: number): void }>(); </script> - Ref 和 Reactive:
ref会自动从初始值推断类型。对于复杂的reactive对象,可以使用接口:interface UserState { name: string; age: number; } const state = reactive<UserState>({ name: ‘’, age: 0 });
掌握这些框架特定的类型技巧,能让你的组件开发更加得心应手,并充分利用 TypeScript 的类型安全特性。
6. 面试高频难题与深度追问实录
6.1 类型兼容性:结构化类型 vs 名义类型
这是 TypeScript 类型系统的一个核心且容易让人困惑的点。TypeScript 使用的是结构化类型(或称为“鸭子类型”),只要结构兼容,类型就是兼容的,而不关心它们是否由同一个类或接口显式声明。
interface Point { x: number; y: number; } interface NamedPoint { x: number; y: number; name: string; } let p1: Point = { x: 0, y: 0 }; let p2: NamedPoint = { x: 0, y: 0, name: “Origin” }; p1 = p2; // 正确!因为 NamedPoint 拥有 Point 的所有属性(x, y) // p2 = p1; // 错误!因为 p1 缺少 name 属性这与 Java、C# 等语言的名义类型系统不同(名义类型要求显式声明继承关系)。面试官可能会问:“为什么p1 = p2是合法的?” 你需要解释结构化类型的规则:如果Y至少拥有与X相同的属性(且对应属性类型兼容),那么Y可以赋值给X。
6.2 协变、逆变与双向协变
这是关于函数类型兼容性的高级话题。考虑函数A = (arg: ArgA) => RetA和B = (arg: ArgB) => RetB,何时B能赋值给A?
- 返回值类型是协变的:
RetB必须是RetA的子类型(或相同类型)。因为调用者期望得到RetA,而B返回了更具体的RetB,这是安全的。 - 参数类型是逆变的:
ArgA必须是ArgB的子类型(或相同类型)。因为B函数声明它处理ArgB类型的参数,而实际调用时传入的是ArgA(ArgA是ArgB的子集,拥有更多约束/属性),B函数内部可能只用到ArgB定义的属性,这些属性ArgA肯定都有,所以是安全的。但在 TypeScript 中,默认情况下(strictFunctionTypes为false时)参数是双向协变的(为了兼容一些常见模式),开启严格模式后才是逆变的。
一个例子:
class Animal { name: string; } class Dog extends Animal { bark() {} } declare let f1: (arg: Animal) => Animal; declare let f2: (arg: Dog) => Dog; // 参数逆变:期望 (Animal)=>Animal, 提供 (Dog)=>Dog 是否安全? // 不安全!因为 f1 可能被这样调用:f1(new Animal())。但 f2 的实现((arg: Dog)=>Dog)期望一个 Dog,传入 Animal 可能缺少 bark 属性。 // 严格模式下,这是错误的。 // 返回值协变:期望 Animal, 返回 Dog,安全。理解这个概念有助于理解更复杂的泛型函数类型兼容性问题。
6.3 声明合并的陷阱与妙用
如前所述,interface支持声明合并。这既是强大的特性,也可能带来陷阱。
陷阱:如果不小心,可能会意外合并类型,导致行为不符合预期。尤其是在扩展第三方库类型或全局对象时。
妙用:
- 扩展第三方库类型:为已有的库(如
window)添加自定义属性。// 在项目的 .d.ts 文件中 interface Window { myCustomProp: string; } // 之后就可以安全地使用 window.myCustomProp - 为类添加静态方法或实例方法(通过合并到类的接口)。
class MyClass { instanceMethod() {} } interface MyClass { staticMethod(): void; } // 合并,为类添加静态方法类型 MyClass.staticMethod = function() { console.log(‘static’); };
面试中可能会让你解释或举例说明声明合并,并询问其利弊。
6.4 类型体操挑战题解析
面试官有时会出一些“类型体操”题来考察你对类型系统的深入理解。例如:
题目:实现一个DeepReadonly<T>工具类型,将嵌套对象的所有属性都变为只读。
type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]; };这里使用了映射类型[P in keyof T]遍历所有属性,并对每个属性值T[P]进行条件判断:如果它是object(注意这里简化了,对于数组、函数等可能需要更精确的判断,如T[P] extends Record<any, any>),则递归应用DeepReadonly;否则,直接使用原类型,并加上readonly修饰符。
题目:实现一个PromiseAll函数,使其返回一个 Promise,其 resolved 值的类型是所有传入 Promise 的 resolved 值类型组成的元组。
declare function PromiseAll<T extends any[]>(values: readonly […T]): Promise<{ [K in keyof T]: Awaited<T[K]>; }>; // 使用 Awaited<T> 内置类型(TS 4.5+)来获取 Promise 的内部类型,或者自己实现一个 UnwrapPromise。这题考察了元组、映射类型、条件类型(Awaited)和readonly修饰符的综合运用。
面对这类题目,不要慌张。拆解问题,从简单情况开始思考,逐步利用你知道的工具类型(keyof,in,extends,infer, 映射类型,条件类型)进行组合。即使不能完全写对,展示你的思考过程和对这些基础工具的理解,也能获得加分。
TypeScript 的深度远不止于此,但掌握以上这些核心概念、高级特性、工程实践和问题排查技巧,足以让你在绝大多数面试中游刃有余,展现出扎实的基本功和解决实际问题的能力。记住,面试不仅是考察知识点,更是考察你如何运用这些知识来思考、设计和解决问题。多动手写代码,多思考类型设计,是提升 TypeScript 水平的不二法门。