news 2026/8/17 15:07:17

TypeScript面试深度解析:从类型系统到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript面试深度解析:从类型系统到工程实践

1. 项目概述:一份面向实战的TypeScript面试深度解析

最近几年,前端技术栈的深度和广度都在飞速扩展,TypeScript 从一个可选的“甜点”变成了许多中大型项目的“标配”。随之而来的,是面试中对 TypeScript 考察的权重越来越高。很多朋友,尤其是从 JavaScript 转过来的开发者,面对 TypeScript 面试题时,常常感觉“概念都懂,但一深问就懵”,或者“知道有泛型、装饰器,但被问到实际应用场景和原理就卡壳”。

我经历过不少面试,也作为面试官考察过很多人,发现一个普遍现象:大家刷题时往往只记住了“答案”,比如anyunknown的区别是“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类型通常出现在以下场景:

  1. 抛出错误的函数:function error(message: string): never { throw new Error(message); }
  2. 无限循环的函数:function infiniteLoop(): never { while (true) {} }
  3. 类型收窄中排除所有可能性的分支:
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中处理,那么shapedefault分支中将不会是never,会导致const _exhaustiveCheck: never = shape;这一行报错,从而提醒你还有未处理的 case。这是一个利用类型系统实现“穷尽性检查”的经典技巧。

2.3 接口与类型别名的微妙差异与选用策略

interfacetype都可以用来定义对象形状,面试中常被问及区别。

核心区别:

  1. 扩展方式interface使用extends进行继承,type使用&(交叉类型)进行组合。

    interface Animal { name: string; } interface Bear extends Animal { honey: boolean; } type Animal = { name: string; }; type Bear = Animal & { honey: boolean; };
  2. 声明合并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对象)或全局模块添加自定义属性。

  3. 能力范围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组件可以渲染任何类型的数组,同时保持itemrenderItem函数内的类型安全。

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

内置工具类型ReturnTypeParameters就是利用条件类型和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时,nullundefined只能赋值给它们自身和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为后缀,里面只包含类型声明,没有具体的实现。

声明文件的来源:

  1. 库自带:越来越多的库在 npm 包中直接包含了类型声明(在package.json中通过“types”“typings”字段指定)。
  2. DefinitelyTyped:一个庞大的社区仓库,为没有自带类型的 JavaScript 库提供高质量的 TypeScript 类型定义。通过@types/前缀的包安装,例如npm install --save-dev @types/lodash
  3. 自己编写:如果使用的库既没有自带类型,也没有@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”这是最常见的错误之一。原因可能是:

  • 变量类型推断错误,实际不是你以为的对象。使用类型断言或修改代码逻辑确保类型正确。
  • 访问了可能为undefinednull的属性。需要使用可选链?.或非空断言!(慎用)或进行判空。
    // 可选链 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-loadertranspileOnly模式:在 Webpack 中,如果不需要类型检查(可以交给 IDE 或单独的命令),可以开启此模式大幅提升编译速度,类型检查通过fork-ts-checker-webpack-plugin在另一个进程进行。

4. 项目引用(Project References)对于大型单体仓库(Monorepo),可以使用tsconfig.json中的references字段来建立项目间的依赖关系。这允许 TypeScript 理解项目结构,实现智能的增量构建和类型检查,避免重复编译依赖项。

5.3 在 React 与 Vue 3 中的最佳实践

React with TypeScript:

  • 组件 Props 类型定义:使用interfacetype明确定义 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:使用definePropsdefineEmits宏,它们支持基于类型的声明。
    <script setup lang=“ts”> interface Props { msg: string; } const props = defineProps<Props>(); const emit = defineEmits<{ (e: ‘change’, id: number): void }>(); </script>
  • Ref 和 Reactiveref会自动从初始值推断类型。对于复杂的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) => RetAB = (arg: ArgB) => RetB,何时B能赋值给A

  • 返回值类型是协变的RetB必须是RetA的子类型(或相同类型)。因为调用者期望得到RetA,而B返回了更具体的RetB,这是安全的。
  • 参数类型是逆变的ArgA必须是ArgB的子类型(或相同类型)。因为B函数声明它处理ArgB类型的参数,而实际调用时传入的是ArgAArgAArgB的子集,拥有更多约束/属性),B函数内部可能只用到ArgB定义的属性,这些属性ArgA肯定都有,所以是安全的。但在 TypeScript 中,默认情况下(strictFunctionTypesfalse时)参数是双向协变的(为了兼容一些常见模式),开启严格模式后才是逆变的。

一个例子:

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支持声明合并。这既是强大的特性,也可能带来陷阱。

陷阱:如果不小心,可能会意外合并类型,导致行为不符合预期。尤其是在扩展第三方库类型或全局对象时。

妙用:

  1. 扩展第三方库类型:为已有的库(如window)添加自定义属性。
    // 在项目的 .d.ts 文件中 interface Window { myCustomProp: string; } // 之后就可以安全地使用 window.myCustomProp
  2. 为类添加静态方法或实例方法(通过合并到类的接口)。
    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 水平的不二法门。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/17 15:07:11

MATLAB控制系统建模:tf、bode、c2d函数实战指南

1. 项目概述&#xff1a;从传递函数到系统分析的完整工具箱 在控制系统、信号处理乃至电路设计的日常工作中&#xff0c;我们常常需要面对一个核心问题&#xff1a;如何描述、分析和实现一个动态系统&#xff1f;无论是设计一个稳定的机器人控制器&#xff0c;还是分析一个音频…

作者头像 李华
网站建设 2026/8/17 15:06:39

CentOS服务器时间同步:Chrony配置与运维实战指南

1. 项目概述&#xff1a;为什么服务器时间同步是运维的“生命线” 刚接手一台新的CentOS服务器&#xff0c;或者管理着一个集群&#xff0c;你最先检查的是什么&#xff1f;CPU、内存、磁盘&#xff1f;这些固然重要&#xff0c;但有一个基础得不能再基础&#xff0c;却又极其关…

作者头像 李华
网站建设 2026/8/17 15:05:05

树莓派4B串口配置全攻略:启用6个UART与避坑指南

1. 项目缘起&#xff1a;为什么树莓派4B的串口配置是个“技术活”&#xff1f; 如果你手头有一块树莓派4B&#xff0c;想用它连接个GPS模块、调试个单片机、或者搞个工业级的RS485通信&#xff0c;第一反应可能就是去用它的GPIO引脚上那对标注为 TXD 和 RXD 的串口。但当你…

作者头像 李华
网站建设 2026/8/17 15:04:13

雷电大漠多线程增强版高性能辅助开发框架(完整开源版)

温馨提示&#xff1a;文末有联系方式 框架核心定位 本框架为雷电模拟器与大漠插件深度整合的多线程增强型辅助开发解决方案&#xff0c;全面优化并发处理能力与执行稳定性&#xff0c;适用于游戏自动化、批量操作等技术研究场景。 即装即用&#xff0c;原创保障 购买后无需二…

作者头像 李华
网站建设 2026/8/17 15:03:07

网易云 NCM 文件打不开?用 ncmdump 免费转 MP3,一次拖放批量搞定

网易云 NCM 文件打不开&#xff1f;用 ncmdump 免费转 MP3&#xff0c;一次拖放批量搞定 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 开车出门前整理歌单&#xff0c;是很多人的日常。但如果你习惯在网易云下载歌曲&#xff0c;大…

作者头像 李华
网站建设 2026/8/17 14:56:05

微分方程建模实战:从动态系统描述到数值求解与模型分析

1. 从“纸上谈兵”到“实战利器”&#xff1a;微分方程在建模中的角色转变 很多人一听到“微分方程”&#xff0c;第一反应就是高等数学课本里那些复杂的符号、抽象的推导和让人头疼的解题技巧。这感觉就像学了一身绝世武功&#xff0c;却不知道在哪个江湖里能用上。我当年也有…

作者头像 李华