Menu

TypeScript 面试题及答案(25 道,附代码)

25 道 TypeScript 面试题,每道都有简短准确的答案和代码,按从入门到进阶分组:any 与 unknown、interface 与 type、泛型、类型收窄、工具类型、映射类型与条件类型、结构类型、tsconfig 以及 TypeScript 7。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

下面是面试官最常问的 TypeScript 问题,每道题都附有你应该能说出的简短答案和一个小示例。题目按难度分组:初级岗位通常只问到第一组,高级岗位则希望你能凭记忆写出最后一组里的类型。

入门题

1. TypeScript 是什么?

TypeScript 是带静态类型的 JavaScript,由 Microsoft 开发。你给值标注类型,编译器检查这些类型,然后把类型去掉,输出可以在任何 JavaScript 环境中运行的普通 JavaScript。

如果没有 @ts-expect-error 注释,第二次调用就是编译错误,这个 bug 根本不会上线。

2. TypeScript 会在运行时检查类型吗?

不会。类型在编译时被擦除,输出中没有留下任何痕迹。上面的程序打印 23,就是因为运行时没有任何东西知道 a 应该是数字。要检查来自程序外部的数据(JSON、用户输入、API 响应),你需要编写真正的检查,例如类型守卫或 schema 校验器。

3. 有哪些基本类型?

string、number(整数和浮点数共用一个类型,没有 int)、boolean、bigint、symbol、null 和 undefined,此外还有数组(number[])、元组([string, number])、对象类型、any、unknown、never 和 void。要使用小写的名字:String 和 Number 是包装对象类型。

4. any 和 unknown 有什么区别?

两者都接受任何值。any 还会关闭类型检查,所以任何操作都能编译通过。unknown 在你收窄它之前什么操作都不允许,因此它是尚未检查的值的安全类型。

5. 什么是类型推断?

编译器会推算出你没有写出的类型:let count = 0 是 number,const mode = "dark" 是字面量类型 "dark",函数的返回类型来自它的 return 语句。通常的做法是给函数参数和公开的返回类型加注解,局部变量交给类型推断。

6. interface 和 type 有什么区别?

两者都能描述对象的形状,类也可以 implements 其中任何一个。interface 可以重新打开并合并(声明合并),也可以 extends 其他接口。type 别名可以给任何东西命名:联合类型、元组、基本类型、映射类型和条件类型。

interface User { name: string }
interface User { age: number }       // merged: User has name and age

type Id = string | number;           // only a type alias can be a union
type Pair = [string, number];        // or a tuple
type Admin = User & { role: "admin" };

常见的约定是对象形状用 interface,其他一切用 type。完整的对比见 interface 与 type 页面。

7. 什么是联合类型和交叉类型?

联合类型 A | B 表示值是其中某一个类型;在收窄之前,你只能使用所有类型共有的成员。交叉类型 A & B 表示值同时是两者,拥有各自的全部成员。

type Id = string | number;                 // either
type Timestamped = { createdAt: Date };
type Post = { title: string } & Timestamped; // both: title and createdAt

8. void 和 never 有什么区别?

void 是正常返回但没有有用返回值的函数的返回类型。never 是不可能发生的事情的类型:总是抛出异常或无限循环的函数,或者所有情况都被排除后的联合类型。never 可以赋值给任何类型,而没有任何值可以赋值给 never。

function log(msg: string): void { console.log(msg); }
function fail(msg: string): never { throw new Error(msg); }
type Impossible = string & number; // never

中级题

9. 什么是类型收窄?

类型收窄就是编译器跟随你的检查,在每个分支内把类型精确化。它能理解 typeof、instanceof、in、相等性检查、真值判断以及用户自定义的类型守卫。

10. 什么是用户自定义类型守卫?

返回类型为 value is T 的函数。当它返回 true 时,调用方的变量就被收窄为 T。编译器信任这个函数,所以函数体必须真正检查这个值。

interface Cat { meow(): void }

function isCat(value: unknown): value is Cat {
    return (
        typeof value === "object" &&
        value !== null &&
        "meow" in value &&
        typeof value.meow === "function"
    );
}

断言函数 function assertCat(v: unknown): asserts v is Cat 则通过抛出异常而不是返回 false 来收窄类型。

11. 什么是泛型?

泛型是类型参数,它让同一个函数、类或类型可以用于多种类型,同时保留输入和输出之间的联系。T 从实参中推断出来,extends 约束它可以是什么。

pluck(users, "email") 会是编译错误,因为 "email" 不在用户类型的 keyof 中。

12. keyof 和 typeof 在类型中有什么作用?

keyof T 是 T 的属性名组成的联合类型。在类型位置上,typeof x 得到变量的类型。两者合起来,keyof typeof obj 把一个对象的键变成联合类型。

const colors = { red: "#f00", green: "#0f0" };
type Colors = typeof colors;        // { red: string; green: string }
type ColorName = keyof typeof colors; // "red" | "green"

13. 什么是工具类型?请举几个例子。

工具类型是内置的泛型类型,用来转换其他类型。最常被问到的有:

工具类型结果
Partial<T>每个属性都变为可选
Required<T>每个属性都变为必选
Readonly<T>每个属性都变为 readonly
Pick<T, "a" | "b">只保留列出的属性
Omit<T, "a">保留除列出属性之外的所有属性
Record<K, V>键为 K、值为 V 的对象
Exclude<U, X>、Extract<U, X>移除或保留联合类型的成员
NonNullable<T>去掉 null 和 undefined 的 T
ReturnType<F>、Parameters<F>函数的返回类型或参数类型
Awaited<T>Promise 解析得到的值

完整列表见 工具类型 页面。

14. 什么是可辨识联合类型?

由多个对象类型组成的联合类型,它们共享一个字面量“标签”属性。检查这个标签就能把值收窄到其中一个成员,而在 default 分支里做一次 never 检查,编译器就会报告你遗漏的任何情况。

15. as 和 satisfies 有什么区别?

value as T 是类型断言:它让编译器把这个值当作 T,并跳过大部分检查。value satisfies T 会按 T 检查这个值,但保留值本身更精确的类型。

type Theme = { primary: string; secondary: string };

const a = { primary: "#07f" } as Theme;        // compiles: the missing key is not reported
const b = { primary: "#07f" } satisfies Theme; // error TS2741: Property 'secondary' is missing
const c = {} as { name: string };              // compiles; c.name is undefined at run time

对象完整之后,satisfies 还会保留它自己推断出的类型:按 Record<string, string> 检查时,变量仍然确切知道自己有哪些键,而类型注解会把它放宽成任意字符串键。

16. 变量后面的 ! 运算符有什么作用?

这是非空断言:el! 从类型中去掉 null 和 undefined。它不会生成任何检查,所以如果值实际上是 null,程序会在使用它的地方崩溃。应优先使用 ?.、?? 或显式的 if。

17. private 和 #private 有什么区别?

private 只由编译器强制执行;在输出的 JavaScript 中,这个属性就是普通属性。#field 是 JavaScript 私有字段,由运行时强制执行。

protected 和 private 类似,但也允许子类访问;readonly 则禁止在构造之后重新赋值。

18. 抽象类和接口有什么区别?

接口只是一个类型:它描述形状,并从输出中消失。抽象类是一个不能实例化的真实类;它可以包含已实现的方法、字段和构造函数,以及子类必须实现的 abstract 成员。一个类可以实现多个接口,但只能继承一个类。

abstract class Repository<T> {
    protected items: T[] = [];
    abstract validate(item: T): boolean; // subclasses must implement
    add(item: T): void {                 // shared implementation
        if (this.validate(item)) this.items.push(item);
    }
}

19. TypeScript 中的函数重载是什么?

先写几个调用签名,再写一个与它们全部兼容的实现。调用方只能看到重载签名,因此每次调用都能得到精确的返回类型。

function parse(value: string): number;
function parse(value: string[]): number[];
function parse(value: string | string[]): number | number[] {
    return Array.isArray(value) ? value.map(Number) : Number(value);
}

const one = parse("4");          // number
const many = parse(["1", "2"]);  // number[]

如果返回类型不取决于参数类型,用一个带联合类型参数的签名会更简单。

进阶题

20. 什么是结构类型?

TypeScript 按形状比较类型,而不是按名字。任何具备所需属性的值都会被接受,即使它从未被声明为那个类型。

这里多出来的 color 没有问题,因为 pixel 是一个变量。直接传入对象字面量 show({ x: 3, y: 4, color: "red" }) 则会报多余属性错误,这项检查只针对新创建的字面量。当两个形状相同的类型不能混用时(UserId 和 OrderId 都是字符串),使用品牌类型:type UserId = string & { readonly __brand: "UserId" }。

21. 如何自己实现 Readonly 和 Partial?

用映射类型:遍历 keyof T,给每个属性加上修饰符。

type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPartial<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // "-" removes a modifier

type User = { name: string; age: number };
type Draft = MyPartial<User>; // { name?: string; age?: number }

22. 什么是条件类型和 infer?

T extends U ? X : Y 根据条件选择一个类型。在条件内部,infer 声明一个类型变量,捕获匹配到的类型的一部分。条件类型会对联合类型进行分配。

type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends (infer E)[] ? E : T;

type A = MyReturnType<() => Promise<number>>; // Promise<number>
type B = ElementOf<string[]>;                 // string
type C = ElementOf<number | boolean[]>;       // number | boolean (distributed)

更多用法见 条件类型 页面。

23. .d.ts 文件是什么,declare 有什么作用?

声明文件只包含在别处存在的代码的类型,例如 JavaScript 库或浏览器 API。declare 声明一个值存在,但并不创建它:declare const VERSION: string; 编译后什么也没有。库的类型要么随包一起提供,要么来自 @types/{name} 包;对于你自己的代码,tsc 在 declaration: true 时会生成 .d.ts 文件。

24. strict 有什么作用,哪些 tsconfig 选项最重要?

"strict": true 会开启 noImplicitAny、strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitThis、useUnknownInCatchVariables 和 strictBuiltinIteratorReturn。在 TypeScript 7 中它默认开启。面试官还会问的其他选项:

选项一句话回答
target输出使用哪个 JavaScript 版本
module输出的模块格式:nodenext 遵循 Node 的规则,esnext/preserve 为打包工具保留 import
moduleResolution如何查找导入:nodenext 在 ES 模块文件中要求写 ./file.js,bundler 接受 ./file
noUncheckedIndexedAccessarr[i] 的类型包含 undefined
paths只给类型检查器用的导入别名;输出中保留别名,所以必须由打包工具或 Node 的 imports 字段来解析
skipLibCheck跳过 .d.ts 文件的检查,以提高速度
verbatimModuleSyntax只作为类型使用的导入必须标记 type;其他导入按原样保留

25. TypeScript 7 是什么?

用 Go 重写的原生 TypeScript 编译器,作为 typescript npm 包的 7 版本发布。命令仍然是 tsc,语言也没有变化,但它比基于 JavaScript 的 6.x 编译器快得多:TypeScript 团队报告称,得益于原生代码和共享内存多线程,典型的完整构建速度提升 8 到 12 倍。它还正式移除了 6.0 已弃用的选项;例如 baseUrl、outFile、moduleResolution: "node10"、module: "amd" 和 alwaysStrict: false 现在都会报错(TS5102、TS5108),提示你删除它们。

常见问题

最常见的 TypeScript 面试题有哪些?

出现最多的是:any 和 unknown 的区别、interface 与 type、泛型如何工作、类型收窄和类型守卫如何工作、Partial、Pick、Omit 这类工具类型的作用,以及 TypeScript 是否在运行时检查类型(不会)。

高级开发者的 TypeScript 面试会问什么?

不仅要会读类型,还要会写类型:用映射类型和条件类型实现 Readonly 或 ReturnType,解释结构类型以及什么时候使用品牌类型,设计一个带穷尽检查的可辨识联合类型,并讨论 tsconfig 的选择,例如 strict、noUncheckedIndexedAccess 和模块解析。

如何准备 TypeScript 面试?

在开启 strict 的情况下写一些小程序,反复阅读编译器错误,直到你能预测它们。要能解释类型收窄、带约束的泛型、主要的工具类型,以及类型断言为什么不安全,每一点都配一个两行的示例。

React 和 Angular 面试会问 TypeScript 吗?

通常会。Angular 本身用 TypeScript 编写,Angular 应用也用它编写,所以 Angular 面试一般默认你会 TypeScript,包括 @Component 这样的装饰器和访问修饰符。使用 TypeScript 代码库的 React 面试,常会让你为 props、state 和事件处理函数标注类型,用到的正是本文展示的接口、联合类型和泛型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始