Skip to content

TypeScript 面试题

本题库共收录 93 道面试题(基础 26 / 进阶 31 / 深入 28 / 架构 8)。 本文件收录 TypeScript 相关面试题,目标题量 200 道。 题型覆盖:概念题、代码分析题、手写代码题、场景设计题。 难度覆盖:基础、进阶、深入、架构。

目录


基础题(19 道)

FB-02-CO-B-001:TypeScript 与 JavaScript 的关系是什么?为什么要使用 TypeScript?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:TypeScript、javascript、静态类型、编译、类型安全 出现频率:高频 预计回答时长:2-3 分钟

题目描述: 请说明 TypeScript 与 JavaScript 的关系,并列举使用 TypeScript 的主要优势。

参考答案

  • TypeScript 是 JavaScript 的超集(superset),在 JS 基础上添加了静态类型系统、接口、泛型、装饰器等特性。
  • TS 代码需编译为 JS 才能在浏览器或 Node.js 中运行,编译器为 tsc
  • 任何有效的 JS 代码都是合法的 TS 代码(默认情况下)。

主要优势:

  1. 静态类型检查:在编译期发现类型错误,减少运行时异常。
  2. 更好的 IDE 支持:自动补全、跳转定义、重构、智能提示。
  3. 可读性与可维护性:类型即文档,便于大型团队协作。
  4. 更安全的重构:修改接口后,依赖方会立即收到编译错误。
  5. 现代特性提前使用:可使用最新 ECMAScript 提案,编译为兼容目标环境的 JS。
ts
// 编译期即可发现错误
function add(a: number, b: number) {
  return a + b;
}
add(1, '2'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

评分维度

  • 解释清楚超集关系(30%)
  • 说明编译流程(20%)
  • 列举 3 个以上实际优势(50%)

常见错误

  • 认为 TypeScript 会改变 JavaScript 的运行时行为
  • 把 TypeScript 说成一门完全独立的语言
  • 忽略 TypeScript 也需要编译才能运行

延伸追问

  • TypeScript 的类型系统会在运行时做检查吗?为什么?
  • TS 编译后的代码是否包含类型信息?

参考资源

口头回答版

  • TypeScript 是 JavaScript 的超集(superset),在 JS 基础上添加了静态类型系统、接口、泛型、装饰器等特性。 - TS 代码需编译为 JS 才能在浏览器或 Node.js 中运行,编译器为 tsc。 - 任何有效的 JS 代码都是合法的 TS 代码(默认情况下)。 静态类型检查:在编译期发现类型错误,减少运行时异常。

FB-02-CO-B-002:TypeScript 有哪些基础类型?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:基础类型、primitive、any、unknown、never 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请列出 TypeScript 的基础类型,并说明 anyunknownnever 的区别。

参考答案

TypeScript 基础类型包括:

  • 原始类型:stringnumberbooleanbigintsymbolnullundefined
  • 对象类型:object(包括 Object、Array、Function 等更具体的类型)
  • 特殊类型:anyunknownnevervoid
  • 复合类型:arraytupleenumunionintersection

anyunknownnever 区别:

类型含义可赋值可调用/访问属性使用场景
any任意类型,关闭类型检查可赋给任何类型允许任意操作临时兼容旧代码,不推荐滥用
unknown未知类型,类型安全版 any只能赋给 unknown/any使用前必须收窄需要类型守卫后使用
never永不存在的类型不能赋给任何类型(除 never)表示不可能到达的分支,或空联合
ts
let a: any = 1;
a.toFixed(); // 不报错,但运行可能出错

let u: unknown = 1;
// u.toFixed(); // Error: Object is of type 'unknown'
if (typeof u === 'number') {
  u.toFixed(); // OK
}

function throwError(): never {
  throw new Error('error');
}

评分维度

  • 完整列出基础类型(40%)
  • 准确区分 any/unknown/never(40%)
  • 能举例说明使用场景(20%)

常见错误

  • 认为 unknownany 完全相同
  • never 等同于 void
  • 不理解 never 在穷尽性检查中的作用

延伸追问

  • 为什么应该尽量用 unknown 替代 any
  • voidundefined 在函数返回值类型上有什么区别?

参考资源

口头回答版

TypeScript 基础类型包括: - 原始类型:string、number、boolean、bigint、symbol、null、undefined - 对象类型:object(包括 Object、Array、Function 等更具体的类型) - 特殊类型:any、unknown、never、void


FB-02-CO-B-003:什么是类型注解(Type Annotation)和类型推断(Type Inference)?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:类型注解、类型推断、显式类型、隐式类型 出现频率:高频 预计回答时长:2-3 分钟

题目描述: 请解释 TypeScript 中类型注解和类型推断的区别,并说明各自的使用场景。

参考答案

  • 类型注解:显式地为变量、参数、返回值等声明类型。
  • 类型推断:TypeScript 编译器根据赋值或上下文自动推导出类型。
ts
// 类型注解
let count: number = 0;
function greet(name: string): string {
  return `Hello, ${name}`;
}

// 类型推断
let count2 = 0; // 推断为 number
let greeting = greet('Tom'); // 推断为 string

使用场景:

  • 优先使用类型推断,保持代码简洁。
  • 在函数参数、返回值、复杂对象或需要明确语义时使用类型注解。
  • 导出 API、库接口、配置文件等公共契约必须显式注解。

评分维度

  • 说清两者定义(40%)
  • 各举一例(30%)
  • 说明使用场景与最佳实践(30%)

常见错误

  • 所有变量都写类型注解,导致代码冗长
  • 函数参数不写注解,依赖推断导致类型拓宽为 any
  • 混淆类型推断与动态类型

延伸追问

  • 如果声明 let x = null,TypeScript 会推断为什么类型?
  • constlet 在类型推断上有什么区别?

相关题目

口头回答版

  • 类型注解:显式地为变量、参数、返回值等声明类型。 - 类型推断:TypeScript 编译器根据赋值或上下文自动推导出类型。 - 优先使用类型推断,保持代码简洁。 - 在函数参数、返回值、复杂对象或需要明确语义时使用类型注解。

FB-02-CO-B-004:Interface 的作用是什么?如何定义可选属性和只读属性?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:interface、可选属性、Readonly、对象类型 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中 interface 的作用,并说明如何定义可选属性和只读属性。

参考答案

interface 用于定义对象的结构契约,描述对象应该有哪些属性、类型和方法。它支持继承、实现、合并声明等特性。

ts
interface User {
  id: number;           // 必填属性
  name: string;         // 必填属性
  email?: string;       // 可选属性(?)
  readonly createdAt: Date; // 只读属性
}

const user: User = {
  id: 1,
  name: 'Tom',
  createdAt: new Date()
};

user.name = 'Jerry';      // OK
// user.createdAt = new Date(); // Error: cannot assign to 'createdAt' because it is a read-only property
  • 可选属性:属性名后加 ?,表示该属性可以不存在。
  • 只读属性:属性前加 readonly,表示初始化后不能重新赋值(但对对象属性的深层修改不生效)。

评分维度

  • 解释 interface 作用(30%)
  • 正确使用可选属性语法(30%)
  • 正确使用只读属性语法(30%)
  • 说明 readonly 的浅层只读限制(10%)

常见错误

  • readonly 等同于深度不可变
  • 可选属性与 | undefined 混淆
  • 在 interface 中使用 = 赋值语法

延伸追问

  • readonlyconst 有什么区别?
  • 如何实现深度只读?

参考资源

口头回答版

interface 用于定义对象的结构契约,描述对象应该有哪些属性、类型和方法。 它支持继承、实现、合并声明等特性。 - 可选属性:属性名后加 ?,表示该属性可以不存在。 - 只读属性:属性前加 readonly,表示初始化后不能重新赋值(但对对象属性的深层修改不生效)。


FB-02-CO-B-005:Type Alias(类型别名)是什么?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:type alias、类型别名、联合类型、交叉类型 出现频率:高频 预计回答时长:2-3 分钟

题目描述: 请解释 TypeScript 中 type 关键字的作用,并说明类型别名可以定义哪些类型。

参考答案

type 用于创建类型别名(Type Alias),为任意类型起一个名字,提高可读性和复用性。

类型别名可以定义:

  • 原始类型别名:type UserID = string
  • 对象类型:type User = { name: string }
  • 联合类型:type Status = 'pending' | 'success' | 'error'
  • 交叉类型:type AdminUser = User & { role: 'admin' }
  • 元组类型:type Point = [number, number]
  • 函数类型:type Handler = (event: Event) => void
ts
type ID = string | number;
type Status = 'loading' | 'success' | 'error';

type Response<T> = {
  data: T;
  status: Status;
};

const res: Response<User> = {
  data: { name: 'Tom' },
  status: 'success'
};

评分维度

  • 说清 type alias 作用(40%)
  • 列举 3 种以上可定义的类型(40%)
  • 能写出示例(20%)

常见错误

  • 认为 type alias 会创建新类型(只是别名)
  • 用 type 定义 class 并尝试 implements
  • 混淆 type 与 interface 的使用场景

延伸追问

  • type 和 interface 都能定义对象类型,应该选择哪个?
  • type 是否支持声明合并?

相关题目

口头回答版

type 用于创建类型别名(Type Alias),为任意类型起一个名字,提高可读性和复用性。 - 原始类型别名:type UserID = string - 对象类型:type User = { name: string } - 联合类型:type Status = 'pending' | 'success' | 'error'


FB-02-CO-B-006:Interface 与 Type Alias 有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:interface、type alias、声明合并、扩展、交叉类型 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请比较 TypeScript 中 interfacetype(类型别名)的区别,并说明各自更适合的场景。

参考答案

特性InterfaceType Alias
定义对象类型✅ 推荐✅ 可以
联合/交叉/元组/函数类型❌ 不能直接定义联合类型✅ 更灵活
声明合并(Declaration Merging)✅ 支持同名合并❌ 不支持
扩展方式extends& 交叉类型
性能通常更优(对象类型)复杂类型可能更慢
报错信息更友好嵌套复杂类型时较差
ts
// interface 支持声明合并
interface User {
  name: string;
}
interface User {
  age: number;
}
// 合并为 { name: string; age: number }

// type 支持联合类型
type Status = 'success' | 'error';
type Response = { data: unknown } & { status: Status };

建议:

  • 定义对象结构、类实现、需要声明合并时优先使用 interface
  • 需要联合类型、交叉类型、元组、映射类型、条件类型时使用 type

评分维度

  • 说出 3 个以上核心区别(50%)
  • 正确举例说明声明合并(25%)
  • 给出选择建议(25%)

常见错误

  • 认为 interface 完全不能表达 type 的能力
  • 在需要联合类型时仍坚持使用 interface
  • 不了解声明合并的应用场景

延伸追问

  • 第三方库为什么会用声明合并扩展全局类型?
  • interface extendstype & 在错误处理上有何差异?

参考资源

口头回答版

| 特性 | Interface | Type Alias | |------|-----------|------------| | 定义对象类型 | ✅ 推荐 | ✅ 可以 | | 联合/交叉/元组/函数类型 | ❌ 不能直接定义联合类型 | ✅ 更灵活 |


FB-02-CO-B-007:Union Types 和 Intersection Types 有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:union、intersection、联合类型、交叉类型、类型组合 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中联合类型(Union Types)和交叉类型(Intersection Types)的区别,并举例说明。

参考答案

  • 联合类型 A | B:表示值可以是 A 或 B 中的任意一种类型。
  • 交叉类型 A & B:表示值同时满足 A 和 B 两种类型的结构。
ts
// 联合类型
function printId(id: string | number) {
  console.log(id);
}
printId('abc');
printId(123);

// 交叉类型
type Person = { name: string };
type Employee = { employeeId: number };
type Staff = Person & Employee;

const staff: Staff = {
  name: 'Tom',
  employeeId: 1
};

注意:

  • 联合类型访问共有属性才安全,访问非共有属性需要类型收窄。
  • 交叉类型如果存在同名但不同类型属性,会生成 never
ts
type A = { x: string };
type B = { x: number };
type C = A & B; // { x: never }

评分维度

  • 说清两者语义差异(40%)
  • 各举一例(30%)
  • 提到联合类型需要类型收窄(20%)
  • 提到同名属性冲突可能产生 never(10%)

常见错误

  • 把联合类型理解为"同时拥有多种类型"
  • 直接访问联合类型的非共有属性
  • 不理解交叉类型在基本类型上的行为(如 string & numbernever

延伸追问

  • 如何对联合类型进行安全的属性访问?
  • string | numberstring & number 分别代表什么?

参考资源

口头回答版

  • 联合类型 A | B:表示值可以是 A 或 B 中的任意一种类型。 - 交叉类型 A & B:表示值同时满足 A 和 B 两种类型的结构。 - 联合类型访问共有属性才安全,访问非共有属性需要类型收窄。 - 交叉类型如果存在同名但不同类型属性,会生成 never。

FB-02-CO-B-008:Enum 枚举类型有什么特点?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:enum、枚举、常量枚举、const enum 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中 enum 的作用,并说明数字枚举、字符串枚举和常量枚举的区别。

参考答案

enum 用于定义一组有名字的常量集合。

ts
// 数字枚举(默认自增)
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right    // 3
}

// 字符串枚举
enum Status {
  Pending = 'PENDING',
  Success = 'SUCCESS',
  Error = 'ERROR'
}

// 常量枚举(编译时内联,不生成反向映射)
const enum Permission {
  Read = 1,
  Write = 2
}
const p = Permission.Read; // 编译为 const p = 1;

特点:

  • 数字枚举支持反向映射:Direction[0] === 'Up'
  • 字符串枚举不支持反向映射,调试信息更友好。
  • const enum 在编译阶段完全内联,不会生成运行时对象,性能更好,但无法在运行时访问。

评分维度

  • 解释 enum 作用(30%)
  • 区分数字/字符串/常量枚举(40%)
  • 说明反向映射和内联特性(20%)
  • 提到 enum 的争议(10%)

常见错误

  • 在字符串枚举中尝试反向映射
  • 混淆 const enum 和普通 enum 的运行时行为
  • 不了解 enum 编译后会产生额外对象

延伸追问

  • 为什么很多项目推荐用联合字面量类型替代 enum?
  • enum 和 object as const 有什么区别?

参考资源

口头回答版

enum 用于定义一组有名字的常量集合。 - 数字枚举支持反向映射:Direction[0] === 'Up'。 - 字符串枚举不支持反向映射,调试信息更友好。 - const enum 在编译阶段完全内联,不会生成运行时对象,性能更好,但无法在运行时访问。


FB-02-CA-B-001:分析以下代码的类型行为

题型:代码分析题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:any、unknown、never、类型收窄、类型安全 出现频率:高频 预计回答时长:3-5 分钟

题目描述

ts
let a: any = 'hello';
let b: unknown = 'hello';
let c: never;

a.toUpperCase();
b.toUpperCase();

function fn(x: string | number) {
  if (typeof x === 'string') {
    console.log(x.toUpperCase());
  } else {
    console.log(x.toFixed(2));
  }
}

请指出哪些代码会报错,并解释原因。

参考答案

  • a.toUpperCase():不报错。any 关闭类型检查,任何操作都被允许,但运行可能出错。
  • b.toUpperCase():报错。unknown 是类型安全的 any,使用前必须先做类型检查/收窄。
  • let c: never:声明本身不报错,但 never 类型无法被赋值(除 never 本身)。
  • fn 函数内部不报错。通过 typeof 类型守卫,TypeScript 在分支中正确收窄了 x 的类型。

正确写法:

ts
if (typeof b === 'string') {
  b.toUpperCase(); // OK
}

评分维度

  • 正确判断 any 的行为(25%)
  • 正确判断 unknown 需要收窄(35%)
  • 正确判断 never 的含义(20%)
  • 解释类型守卫收窄机制(20%)

常见错误

  • 认为 unknown 和 any 行为相同
  • 忽略 typeof 类型守卫的作用
  • 认为 let c: never 声明会报错

延伸追问

  • 如何为 unknown 编写自定义类型守卫?
  • any 类型会污染周围的类型推断吗?

相关题目

口头回答版

  • a.toUpperCase():不报错。 any 关闭类型检查,任何操作都被允许,但运行可能出错。 - b.toUpperCase():报错。 unknown 是类型安全的 any,使用前必须先做类型检查/收窄。

FB-02-CA-B-002:分析可选属性与默认值的类型行为

题型:代码分析题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:可选属性、默认值、undefined、函数参数 出现频率:中频 预计回答时长:3-5 分钟

题目描述

ts
interface Config {
  host?: string;
  port?: number;
}

function createServer(config: Config = {}) {
  const host = config.host ?? 'localhost';
  const port = config.port ?? 3000;
  return { host, port };
}

createServer();
createServer({ host: 'example.com' });
createServer({ port: '8080' });

请分析上述代码的类型检查情况。

参考答案

  • createServer():合法,使用默认空对象 {}
  • createServer({ host: 'example.com' }):合法,port 保持可选。
  • createServer({ port: '8080' }):报错。port 类型为 number | undefined,不能传入字符串 '8080'

关键点:

  • 可选属性等价于 T | undefined,但传入对象时该属性可以省略。
  • 函数默认参数 {} 允许无参调用。
  • 默认值与可选属性是不同概念:默认值处理运行时的值,可选属性是类型层面的约束。

评分维度

  • 正确分析无参调用(20%)
  • 正确分析部分属性调用(20%)
  • 正确识别类型错误(40%)
  • 解释可选属性与默认值的区别(20%)

常见错误

  • 认为可选属性不能是 undefined
  • 混淆函数参数默认值与对象属性默认值
  • 认为 {} 不能赋值给可选属性对象

延伸追问

  • config: Config = {}config?: Config 有什么区别?
  • 如何为 interface 中所有可选属性设置默认值?

口头回答版

  • createServer():合法,使用默认空对象 {}。 - createServer({ host: 'example.com' }):合法,port 保持可选。 - createServer({ port: '8080' }):报错。 port 类型为 number | undefined,不能传入字符串 '8080'。

FB-02-CO-B-009:Tuple 元组类型是什么?与数组有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:tuple、元组、数组、固定长度、类型位置 出现频率:中频 预计回答时长:2-3 分钟

题目描述: 请解释 TypeScript 中 Tuple 元组类型,并说明它与数组类型的区别。

参考答案

  • 数组类型:元素类型相同,长度可变。number[]
  • 元组类型:元素类型和长度都固定。[string, number]
ts
// 数组
const arr: number[] = [1, 2, 3];
arr.push(4); // OK

// 元组
const tuple: [string, number] = ['Tom', 20];
// tuple[0].toFixed(); // Error: string 没有 toFixed
// tuple.push(true);   // Error: 类型不匹配(但 push 在元组上有特殊处理)

// 可选元组元素
const optional: [string, number?] = ['Tom'];

// 剩余元素
const rest: [string, ...number[]] = ['Tom', 1, 2, 3];

元组常用于表示固定结构的数据,如坐标点、函数返回值、React 的 useState 返回结果等。

评分维度

  • 说清数组和元组的区别(50%)
  • 各举一例(30%)
  • 提到可选元素和剩余元素(20%)

常见错误

  • 把元组当成普通数组使用,不固定长度
  • 混淆 [string, number](string | number)[]
  • 不了解元组 push 的行为限制

延伸追问

  • React useState 的返回类型为什么是元组?
  • 如何用元组表示函数的多个返回值?

口头回答版

  • 数组类型:元素类型相同,长度可变。 - 元组类型:元素类型和长度都固定。 [string, number] 元组常用于表示固定结构的数据,如坐标点、函数返回值、React 的 useState 返回结果等。

FB-02-CD-B-001:手写一个类型安全的通用函数

题型:手写代码题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:函数类型、泛型、类型安全、参数校验 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 请用 TypeScript 实现一个 mapArray 函数,接收一个数组和一个映射函数,返回映射后的新数组。要求类型安全,支持任意输入类型和输出类型。

参考答案

ts
function mapArray<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
  const result: U[] = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(fn(arr[i], i));
  }
  return result;
}

// 使用示例
const numbers = [1, 2, 3];
const strings = mapArray(numbers, (n) => n.toString()); // string[]

const users = [{ name: 'Tom' }, { name: 'Jerry' }];
const names = mapArray(users, (u) => u.name); // string[]

要点:

  • 使用泛型 T 表示输入数组元素类型。
  • 使用泛型 U 表示映射函数返回值类型。
  • 返回类型为 U[],与映射函数返回值一致。
  • 保持原数组不变,不修改输入。

评分维度

  • 正确使用泛型定义输入输出类型(40%)
  • 函数签名类型正确(30%)
  • 实现逻辑正确(20%)
  • 能给出使用示例(10%)

常见错误

  • 使用 any[] 作为返回类型
  • 混淆数组元素类型与索引类型
  • 泛型参数命名不规范

延伸追问

  • 如何让 mapArray 支持 readonly 输入数组?
  • 如果映射函数可能是异步的,应该如何修改类型?

相关题目

口头回答版

  • 使用泛型 T 表示输入数组元素类型。 - 使用泛型 U 表示映射函数返回值类型。 - 返回类型为 U[],与映射函数返回值一致。 - 保持原数组不变,不修改输入。

FB-02-CO-B-010:Type Assertion(类型断言)是什么?与类型转换有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:类型断言、as、类型转换、类型收窄 出现频率:高频 预计回答时长:2-3 分钟

题目描述: 请解释 TypeScript 中的类型断言,并说明它与类型转换的区别。

参考答案

  • 类型断言:告诉编译器"我知道这个值的具体类型",只在编译时生效,不影响运行时行为。
  • 类型转换:运行时改变值的实际类型,如 String(123)Number('123')
ts
const value: unknown = 'hello world';

// 类型断言
const len = (value as string).length;

// 类型转换
const num = Number('123'); // 运行时真的转换

两种断言语法:

  • 尖括号语法:<string>value(在 TSX 中不推荐,会与 JSX 冲突)
  • as 语法:value as string(推荐)

注意:

  • 类型断言不能改变值的实际运行时类型,滥用可能导致运行时错误。
  • 优先使用类型守卫等安全收窄方式。

评分维度

  • 说清类型断言只在编译期生效(40%)
  • 区分类型断言和类型转换(30%)
  • 知道 as 语法和尖括号语法(20%)
  • 提到滥用风险(10%)

常见错误

  • 认为类型断言会进行运行时转换
  • 在 TSX 中使用尖括号断言导致解析错误
  • 用断言绕过类型检查而不做验证

延伸追问

  • as constas 有什么区别?
  • 什么时候应该使用类型断言而不是类型守卫?

口头回答版

  • 类型断言:告诉编译器"我知道这个值的具体类型",只在编译时生效,不影响运行时行为。 - 类型转换:运行时改变值的实际类型,如 String(123)、Number('123')。 - 尖括号语法:<string>value(在 TSX 中不推荐,会与 JSX 冲突) - as 语法:value as string(推荐)

FB-02-CA-B-003:分析联合类型的类型收窄行为

题型:代码分析题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:联合类型、类型收窄、类型守卫、typeof、in 出现频率:高频 预计回答时长:3-5 分钟

题目描述

ts
type Animal = { name: string; bark: () => void };
type Plant = { name: string; photosynthesize: () => void };
type Entity = Animal | Plant;

function describe(entity: Entity) {
  console.log(entity.name);

  if ('bark' in entity) {
    entity.bark();
  } else {
    entity.photosynthesize();
  }
}

请分析这段代码的类型检查行为,并说明 in 操作符在这里的作用。

参考答案

  • entity.name:合法。nameAnimalPlant 的共有属性。
  • if ('bark' in entity):这是 in 操作符类型守卫。如果为 true,TypeScript 将 entity 收窄为 Animal
  • entity.bark():在 if 分支内合法,因为类型已收窄为 Animal
  • entity.photosynthesize():在 else 分支内合法,因为 Entity 中不含有 bark 的只可能是 Plant
ts
// 等价于
type Entity =
  | { name: string; bark: () => void }
  | { name: string; photosynthesize: () => void };

in 类型守卫适用于通过属性存在性区分的联合类型。

评分维度

  • 正确解释共有属性访问(20%)
  • 正确解释 in 类型守卫作用(40%)
  • 正确分析两个分支的类型(30%)
  • 提到 in 守卫的适用场景(10%)

常见错误

  • 认为可以直接访问联合类型的非共有属性
  • 不了解 in 操作符可以作为类型守卫
  • 混淆 intypeofinstanceof

延伸追问

  • in 类型守卫与自定义类型守卫有什么区别?
  • 如果 Animal 和 Plant 有更多重叠属性,如何设计可辨识联合?

相关题目

口头回答版

  • entity.name:合法。 name 是 Animal 和 Plant 的共有属性。 - if ('bark' in entity):这是 in 操作符类型守卫。 如果为 true,TypeScript 将 entity 收窄为 Animal。

FB-02-CD-B-002:手写一个类型安全的 identity 函数

题型:手写代码题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:泛型、identity、类型推断、泛型参数 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请用 TypeScript 实现一个 identity 函数,它接收一个参数并原样返回。要求类型安全,调用时返回值类型与参数类型一致。

参考答案

ts
function identity<T>(arg: T): T {
  return arg;
}

// 显式指定类型参数
const num = identity<number>(42); // number

// 类型推断
const str = identity('hello'); // string
const arr = identity([1, 2, 3]); // number[]

要点:

  • 使用泛型参数 T 捕获调用时传入的具体类型。
  • 返回类型与参数类型相同,都是 T
  • 调用时通常可以省略类型参数,让编译器自动推断。

评分维度

  • 正确定义泛型函数(50%)
  • 返回类型与参数类型一致(30%)
  • 能展示显式和推断两种调用方式(20%)

常见错误

  • 使用 any 作为参数和返回类型
  • 返回类型写成 unknown
  • 泛型参数位置错误

延伸追问

  • 如果要求 identity 只能接受对象类型,应该怎么改?
  • 泛型函数的类型参数在编译后会保留吗?

参考资源

口头回答版

  • 使用泛型参数 T 捕获调用时传入的具体类型。 - 返回类型与参数类型相同,都是 T。 - 调用时通常可以省略类型参数,让编译器自动推断。

FB-02-CO-B-013:any、unknown、never 有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:any、unknown、never、类型安全 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请说明三者的赋值方向、使用场景和类型安全差异。

参考答案

any 关闭类型检查,可任意赋值和调用。unknown 是更安全的 any,赋值给任何类型前必须先做类型收窄。never 表示不可能存在的值,常用于穷尽检查和函数永远抛错/不返回。

let a: any = 1; a.foo();
let u: unknown = 1; (u as number).toFixed();
function fail(): never { throw new Error(); }

评分维度

  • any(30%):关闭检查的风险
  • unknown(40%):使用前必须收窄
  • never(30%):穷尽分支与不可达值

常见错误

  • 认为 unknown 和 any 完全一样
  • 把 never 当成 void 使用
  • 在可能返回值的函数上使用 never

口头回答版

any 关闭类型检查,unknown 需要收窄后才能使用,never 表示不可能存在的值。

FB-02-CO-B-014:interface 与 type 有什么异同?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:interface、type、类型别名、声明合并 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请比较 interface 和 type 在扩展、合并、实现和性能上的差异。

参考答案

interface 支持声明合并和 implements,适合对象类型定义;type 更通用,可定义联合、交叉、元组、映射等,但不能声明合并。大多数对象类型优先用 interface,复杂类型用 type。

同:都可描述对象结构、支持泛型、可互相扩展(interface extends type / type = interface & ...)。

评分维度

  • 声明合并(30%):interface 可多次声明合并
  • 扩展能力(30%):联合、交叉、映射只能用 type
  • 实现(20%):class 可实现 interface
  • 选择原则(20%):对象用 interface,复杂用 type

常见错误

  • 认为 type 也支持声明合并
  • 所有类型都用 type,导致无法 implements 和合并
  • 认为 interface 性能一定优于 type

口头回答版

interface 支持声明合并和 implements,适合对象;type 更灵活,适合联合、交叉和映射类型。

FB-02-CO-B-015:可选属性 ? 与 undefined 在类型上有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:可选属性、undefined、类型系统 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 name?: stringname: string | undefined 的差异。

参考答案

name?: string 表示属性可以不存在,读取时类型为 string | undefined。name: string | undefined 表示属性必须存在,但值可以是 undefined。在 exactOptionalPropertyTypes 开启后,前者不能把 undefined 显式赋值给该属性。

interface A { name?: string; }
interface B { name: string | undefined; }
const a: A = {}; // OK
const b: B = {}; // Error

评分维度

  • 存在性(50%):属性是否必须存在
  • 赋值(30%):能否显式赋 undefined
  • 配置影响(20%):exactOptionalPropertyTypes

常见错误

  • 认为两者完全等价
  • 在解构时使用默认值时忽略属性不存在和值为 undefined 的区别
  • 开启 exactOptionalPropertyTypes 后仍向可选属性赋 undefined

口头回答版

可选属性表示可以不存在,undefined 类型表示必须存在但可为 undefined,二者在 exactOptionalPropertyTypes 下有差异。


FB-02-CO-B-016:TypeScript 中 enum 与 const enum 有什么区别?

题型:概念题 难度:🟢 基础 岗位层级:初级 面试知识域:02 TypeScript 标签:enum、const enum、编译产物、性能 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请说明普通枚举和常量枚举在编译产物、使用场景上的差异。

参考答案

普通 enum 会生成运行时对象和反向映射,占用打包体积。const enum 在编译阶段完全内联,不生成运行时对象,但跨模块编译且 isolatedModules 开启时会报错。

enum Status { Pending, Done }
const enum Role { Admin, User }
const r = Role.Admin; // 编译后替换为 0

评分维度

  • enum 编译产物(40%):运行时对象与反向映射
  • const enum(40%):编译时内联
  • 使用注意(20%):isolatedModules 报错

常见错误

  • const enum 导出到公共 API 导致消费方报错
  • 在需要运行时对象时用 const enum
  • 忽略 enum 对打包体积的影响

口头回答版

enum 生成运行时对象,const enum 编译时内联;const enum 不宜用于公共 API,isolatedModules 下会报错。


进阶题(24 道)

口头回答版

  • 使用泛型参数 T 捕获调用时传入的具体类型。 - 返回类型与参数类型相同,都是 T。 - 调用时通常可以省略类型参数,让编译器自动推断。

FB-02-CO-A-001:什么是泛型约束(Generic Constraints)?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:泛型、约束、extends、关键字 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中泛型约束的作用,并举例说明如何使用。

参考答案

泛型约束用于限制泛型参数必须满足某些条件,确保在泛型内部可以安全访问特定属性或方法。

ts
// 约束 T 必须有 length 属性
function logLength<T extends { length: number }>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength('hello'); // OK,string 有 length
logLength([1, 2, 3]); // OK,array 有 length
// logLength(123); // Error: number 没有 length

常见约束形式:

  • 属性约束:T extends { id: number }
  • 构造函数约束:T extends new (...args: any[]) => any
  • 联合类型约束:T extends string | number
  • 多个约束:通过交叉类型 T extends A & B

评分维度

  • 解释泛型约束作用(40%)
  • 正确写出 extends 语法(30%)
  • 举例说明约束应用场景(30%)

常见错误

  • 在泛型内部直接访问未约束的属性
  • 约束条件写得过于宽泛或过于严格
  • 混淆泛型约束与类的 extends

延伸追问

  • 如何约束泛型参数必须是某个类的子类?
  • keyof 和泛型约束如何配合使用?

相关题目

口头回答版

泛型约束用于限制泛型参数必须满足某些条件,确保在泛型内部可以安全访问特定属性或方法。 - 属性约束:T extends { id: number } - 构造函数约束:T extends new (...args: any[]) => any - 联合类型约束:T extends string | number


FB-02-CD-A-001:手写一个类型安全的 get 工具函数

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:泛型、keyof、索引访问、对象属性、工具函数 出现频率:高频 预计回答时长:5-10 分钟

题目描述: 请实现一个类型安全的 get 函数,根据对象和路径字符串获取嵌套属性值。要求路径字符串必须是对象中存在的有效路径。

进阶:支持嵌套路径,如 get(obj, 'a.b.c')

参考答案

基础版(单层路径):

ts
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Tom', age: 20 };
const n = get(user, 'name'); // string
// const x = get(user, 'email'); // Error: 'email' 不存在

进阶版(嵌套路径):

ts
type PathValue<T, P extends string> =
  P extends `${infer K}.${infer Rest}`
    ? K extends keyof T
      ? PathValue<T[K], Rest>
      : never
    : P extends keyof T
      ? T[P]
      : never;

function get<T extends Record<string, any>, P extends string>(
  obj: T,
  path: P
): PathValue<T, P> {
  return path.split('.').reduce((o, k) => o?.[k], obj);
}

const obj = { a: { b: { c: 1 } } };
const val = get(obj, 'a.b.c'); // number
// get(obj, 'a.b.x'); // 简单版可能无法完全约束嵌套路径,取决于实现

评分维度

  • 基础版类型正确(30%)
  • 使用 keyof 约束 key(30%)
  • 进阶版使用模板字面量类型推导路径(30%)
  • 处理边界情况(10%)

常见错误

  • key 参数类型为 string,失去类型安全
  • 返回值类型为 any
  • 模板字面量类型推导错误

延伸追问

  • 如何支持数组索引路径,如 get(obj, 'users.0.name')
  • 如何处理可选属性路径,使返回类型包含 undefined?

口头回答版

基础版(单层路径): (见代码示例) 进阶版(嵌套路径): (见代码示例)


FB-02-CO-A-002:TypeScript 中有哪些内置的类型保护(Type Guards)?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:类型保护、type guard、typeof、instanceof、in 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请列举 TypeScript 中常见的类型保护方式,并说明它们如何收窄联合类型。

参考答案

类型保护是在运行时检查值,同时帮助 TypeScript 编译器收窄类型的机制。

常见类型保护:

  1. typeof 守卫:用于基本类型(stringnumberbooleansymbolundefinedfunctionbigint)。
  2. instanceof 守卫:用于判断对象是否是某个类的实例。
  3. in 守卫:检查对象是否含有某个属性。
  4. 字面量相等判断=====!==switch
  5. 可辨识联合(Discriminated Unions):通过共有字面量属性区分。
  6. 自定义类型保护:返回 value is Type 的函数。
ts
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function area(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2; // 收窄为 circle
    default:
      return shape.side ** 2; // 收窄为 square
  }
}

评分维度

  • 列举 4 种以上类型保护(50%)
  • 说明各自适用场景(30%)
  • 举例说明可辨识联合(20%)

常见错误

  • typeofnull 返回 'object',不能用来区分 null
  • instanceof 不能用于接口类型(接口只在编译期存在)
  • 对联合类型直接访问非共有属性

延伸追问

  • typeof [] 返回什么?如何用类型保护区分数组和对象?
  • 自定义类型保护函数和普通布尔函数有什么区别?

相关题目

口头回答版

类型保护是在运行时检查值,同时帮助 TypeScript 编译器收窄类型的机制。 typeof 守卫:用于基本类型(string、number、boolean、symbol、undefined、function、bigint)。 instanceof 守卫:用于判断对象是否是某个类的实例。 in 守卫:检查对象是否含有某个属性。


FB-02-CD-A-002:手写一个自定义类型保护函数

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:自定义类型保护、is、类型守卫、运行时检查 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 请实现一个自定义类型保护函数 isUser,用于判断一个 unknown 值是否是符合 User 接口的对象。要求返回类型为 value is User

ts
interface User {
  id: number;
  name: string;
  email?: string;
}

参考答案

ts
interface User {
  id: number;
  name: string;
  email?: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    typeof (value as Record<string, unknown>).id === 'number' &&
    typeof (value as Record<string, unknown>).name === 'string' &&
    ('email' in value === false ||
      typeof (value as Record<string, unknown>).email === 'string')
  );
}

// 使用
function greetUser(input: unknown) {
  if (isUser(input)) {
    console.log(input.name); // 类型已收窄为 User
  }
}

要点:

  • 返回类型标注为 value is User
  • 运行时检查必须足够严格,避免误判。
  • 对于可选属性需要特殊处理。

评分维度

  • 正确使用 value is Type 语法(30%)
  • 运行时检查逻辑完整(40%)
  • 处理 null 和可选属性(20%)
  • 能说明类型保护的优势(10%)

常见错误

  • 返回类型写成 boolean
  • 只做 typeof value === 'object' 检查,不判断 null
  • 不检查必填属性的类型

延伸追问

  • 如何生成更健壮的运行时类型检查?
  • 类型保护函数和 zod/yup 等 schema 验证库有什么区别?

参考资源

口头回答版

  • 返回类型标注为 value is User。 - 运行时检查必须足够严格,避免误判。 - 对于可选属性需要特殊处理。

FB-02-CO-A-003:什么是 Mapped Types(映射类型)?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:mapped types、映射类型、keyof、in、工具类型 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的映射类型,并举例说明如何基于已有类型生成新类型。

参考答案

映射类型允许基于已有类型的键生成新类型,通过索引签名语法遍历 keyof 得到的键集合。

ts
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

映射类型支持修饰符:

  • readonly / -readonly:添加或移除只读
  • ? / -?:添加或移除可选
ts
// 移除所有 readonly
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// 移除所有可选
type Required<T> = {
  [P in keyof T]-?: T[P];
};

评分维度

  • 解释映射类型语法(40%)
  • 举例说明如何生成新类型(30%)
  • 说明 readonly 和 ? 修饰符(30%)

常见错误

  • 把映射类型理解为运行时遍历
  • 混淆 in 在映射类型和 for...in 中的含义
  • 不会使用 -readonly-? 移除修饰符

延伸追问

  • 如何实现 Pick<T, K>Omit<T, K>
  • 映射类型中的 as 关键字有什么作用?

相关题目

口头回答版

映射类型允许基于已有类型的键生成新类型,通过索引签名语法遍历 keyof 得到的键集合。 映射类型支持修饰符: - readonly / -readonly:添加或移除只读 - ? / -?:添加或移除可选


FB-02-CO-A-004:什么是 Conditional Types(条件类型)?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:conditional types、条件类型、extends、三元类型 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的条件类型,并举例说明其用法。

参考答案

条件类型根据类型关系选择类型,语法类似三元运算符:T extends U ? X : Y

ts
// 如果 T 是 string,返回 number,否则返回 boolean
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<123>;     // false

应用场景:

  • 根据类型是否 assignable 到另一类型做分支。
  • 结合 infer 提取类型的一部分。
  • 实现工具类型如 ExcludeExtractNonNullable
ts
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

type C = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type D = Extract<'a' | 'b' | 'c', 'a' | 'd'>; // 'a'

评分维度

  • 解释条件类型语法(40%)
  • 举例基本用法(30%)
  • 提到与工具类型的关系(30%)

常见错误

  • 把条件类型理解为运行时条件
  • 不理解 distributive conditional types(分配条件类型)
  • 不会用 never 过滤联合类型

延伸追问

  • 什么是分配条件类型?如何用 [] 关闭分配?
  • 条件类型中的 never 有什么特殊行为?

相关题目

口头回答版

条件类型根据类型关系选择类型,语法类似三元运算符:T extends U ? X : Y。 - 根据类型是否 assignable 到另一类型做分支。 - 结合 infer 提取类型的一部分。 - 实现工具类型如 Exclude、Extract、NonNullable。


FB-02-CO-A-005:TypeScript 有哪些常用内置工具类型?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:utility types、Partial、Required、Readonly、Pick、Omit、Record 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请列举并解释 TypeScript 中常用的内置工具类型,如 PartialRequiredReadonlyPickOmitRecord

参考答案

常用内置工具类型:

工具类型作用
Partial<T>将 T 的所有属性变为可选
Required<T>将 T 的所有属性变为必填
Readonly<T>将 T 的所有属性变为只读
Pick<T, K>从 T 中挑选 K 指定的属性
Omit<T, K>从 T 中排除 K 指定的属性
Record<K, T>创建键类型为 K、值类型为 T 的对象类型
Exclude<T, U>从 T 中排除可赋值给 U 的类型
Extract<T, U>从 T 中提取可赋值给 U 的类型
NonNullable<T>从 T 中排除 null 和 undefined
ReturnType<T>获取函数 T 的返回类型
Parameters<T>获取函数 T 的参数类型元组
ts
interface User {
  id: number;
  name: string;
  age: number;
}

type UserPreview = Pick<User, 'id' | 'name'>;
type UserForm = Partial<Omit<User, 'id'>>;
type UserMap = Record<number, User>;

评分维度

  • 列举 6 个以上工具类型(50%)
  • 准确解释每个作用(30%)
  • 能写出使用示例(20%)

常见错误

  • 混淆 PickOmit
  • 不清楚 Record 的使用场景
  • 不会组合使用多个工具类型

延伸追问

  • Partial<T>T | undefined 有什么区别?
  • 如何自己实现 DeepPartial<T>

相关题目

口头回答版

| 工具类型 | 作用 | |----------|------| | Partial<T> | 将 T 的所有属性变为可选 | | Required<T> | 将 T 的所有属性变为必填 |


FB-02-CO-A-006:Index Signatures(索引签名)是什么?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:index signature、索引签名、Record、动态键、字符串索引 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的索引签名,并说明它与 Record 的区别和使用场景。

参考答案

索引签名用于描述对象中动态键的类型结构。

ts
interface StringDictionary {
  [key: string]: string;
}

const dict: StringDictionary = {
  name: 'Tom',
  role: 'admin'
};

索引签名类型:

  • [key: string]: T:字符串键索引
  • [key: number]: T:数字键索引(数组常用)
  • [key: symbol]: T:symbol 键索引

限制:

  • 索引签名返回值类型必须是所有已知属性类型的公共父类型。
  • 不能同时使用 stringnumber 索引签名,除非 number 返回类型是 string 返回类型的子类型。
ts
interface Mixed {
  [key: string]: number | string;
  [key: number]: number; // OK,number 是 number | string 的子类型
}

Record<K, T> 是索引签名的类型安全替代,要求键类型明确。

评分维度

  • 解释索引签名语法(40%)
  • 说明与具体属性的兼容性(30%)
  • 区分索引签名和 Record(20%)
  • 提到 string/number 索引签名限制(10%)

常见错误

  • 索引签名返回类型与具体属性类型冲突
  • 用索引签名代替 interface 的明确属性定义
  • 混淆 Record<string, T>{ [key: string]: T }

延伸追问

  • 如何限制对象的键只能是特定的字符串联合?
  • 索引签名与 Object.keys() 的类型有什么关系?

口头回答版

索引签名用于描述对象中动态键的类型结构。 - [key: string]: T:字符串键索引 - [key: number]: T:数字键索引(数组常用) - [key: symbol]: T:symbol 键索引


FB-02-CO-A-007:Function Overloads(函数重载)是什么?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:function overloads、函数重载、签名、多态 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的函数重载,并说明它与联合类型参数的区别。

参考答案

函数重载允许一个函数根据传入参数的不同类型返回不同的类型,提供多个函数签名。

ts
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: number | string, b: number | string): number | string {
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
  }
  return String(a) + String(b);
}

const n = add(1, 2);       // number
const s = add('a', 'b');   // string

与联合类型参数的区别:

  • 函数重载:调用时返回类型更精确。
  • 联合类型参数:返回类型通常是联合类型,调用后需要进一步收窄。

评分维度

  • 解释重载语法和作用(40%)
  • 正确写出重载示例(30%)
  • 区分重载与联合参数(30%)

常见错误

  • 重载签名与实现签名不匹配
  • 重载顺序不当(更具体的签名应在前)
  • 用重载替代更简单的泛型或联合类型

延伸追问

  • 重载函数的实现签名对外可见吗?
  • 类的方法可以重载吗?

参考资源

口头回答版

函数重载允许一个函数根据传入参数的不同类型返回不同的类型,提供多个函数签名。 与联合类型参数的区别: - 函数重载:调用时返回类型更精确。 - 联合类型参数:返回类型通常是联合类型,调用后需要进一步收窄。


FB-02-CA-A-001:分析以下泛型代码的推断结果

题型:代码分析题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:泛型、类型推断、条件类型、infer、返回类型 出现频率:高频 预计回答时长:5-8 分钟

题目描述

ts
function createPair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const pair = createPair('hello', 42);

// ---

type Return<T> = T extends (...args: any[]) => infer R ? R : never;

function fetchUser() {
  return { id: 1, name: 'Tom' };
}

type FetchUserReturn = Return<typeof fetchUser>;

请分析 pairFetchUserReturn 的类型分别是什么。

参考答案

  • pair 的类型是 [string, number]

    • T 被推断为 'hello',再拓宽(widening)为 string
    • U 被推断为 42,再拓宽为 number
    • 返回类型为 [string, number]
  • FetchUserReturn 的类型是 { id: number; name: string; }

    • typeof fetchUser 是函数类型 () => { id: number; name: string }
    • 条件类型 T extends (...args: any[]) => infer R ? R : never 匹配函数签名,推断 R 为返回类型。
    • 因此最终类型为 { id: number; name: string }

评分维度

  • 正确推断 pair 类型(30%)
  • 正确解释泛型推断过程(25%)
  • 正确推断 FetchUserReturn 类型(25%)
  • 解释 infer 在条件类型中的作用(20%)

常见错误

  • 认为 pair 是 ['hello', 42] 字面量类型
  • 不理解 infer 关键字的含义
  • 混淆 typeof 在值和类型上下文中的用法

延伸追问

  • 如何让 pair 保持字面量类型 ['hello', 42]
  • ReturnType<T> 和上面的 Return<T> 有什么区别?

相关题目

口头回答版

  • pair 的类型是 [string, number]。 - T 被推断为 'hello',再拓宽(widening)为 string。 - U 被推断为 42,再拓宽为 number。 - 返回类型为 [string, number]。

FB-02-CO-A-008:keyoftypeof 操作符在类型上下文中有什么作用?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:keyof、typeof、索引类型、类型查询、类型操作符 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中 keyoftypeof 操作符的作用,并举例说明它们如何配合使用。

参考答案

  • keyof T:获取类型 T 的所有公共属性键的联合类型。
  • typeof v:在类型上下文中,获取变量 v 的类型。
ts
interface User {
  id: number;
  name: string;
  age: number;
}

type UserKeys = keyof User; // 'id' | 'name' | 'age'

const user = {
  id: 1,
  name: 'Tom'
};

type UserType = typeof user; // { id: number; name: string }
type UserKey = keyof typeof user; // 'id' | 'name'

常见组合:

  • keyof typeof obj:获取对象字面量的键联合。
  • T[K]:索引访问类型,获取 T 中 K 对应的属性类型。
ts
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

评分维度

  • 准确解释 keyof 作用(35%)
  • 准确解释 typeof 在类型上下文中的作用(35%)
  • 能举例说明组合使用(30%)

常见错误

  • 在值上下文中误用 keyof
  • 混淆 JS 的 typeof 和 TS 的 typeof(后者在类型上下文中)
  • 认为 keyof 可以获取私有属性

延伸追问

  • keyof any 的结果是什么?
  • 如何获取一个数组的元素类型?

相关题目

口头回答版

  • keyof T:获取类型 T 的所有公共属性键的联合类型。 - typeof v:在类型上下文中,获取变量 v 的类型。 - keyof typeof obj:获取对象字面量的键联合。 - T[K]:索引访问类型,获取 T 中 K 对应的属性类型。

FB-02-CO-A-009:TypeScript 类的访问修饰符有哪些?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:class、访问修饰符、public、private、protected、Readonly 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中类的访问修饰符 publicprivateprotectedreadonly 的区别。

参考答案

修饰符作用
public默认修饰符,任何地方都可访问
private只能在类内部访问(编译期检查,运行时可通过如 # 以外的手段访问)
protected类内部及子类中可访问
readonly只能在声明时或构造函数中赋值,之后只读
ts
class Animal {
  public name: string;
  private age: number;
  protected species: string;
  readonly id: number;

  constructor(name: string, age: number, species: string, id: number) {
    this.name = name;
    this.age = age;
    this.species = species;
    this.id = id;
  }
}

class Dog extends Animal {
  bark() {
    console.log(this.species); // OK,protected 可在子类访问
    // console.log(this.age);    // Error,private 不可在子类访问
  }
}

注意:

  • TS 的 private 是编译期概念,JS 私有字段应使用 #field
  • 构造函数参数上加修饰符可自动创建同名属性。

评分维度

  • 区分 4 种修饰符(60%)
  • 能举例说明访问范围(30%)
  • 提到 private 与 JS #field 的区别(10%)

常见错误

  • 认为 private 在运行时也能完全隔离访问
  • 混淆 protected 和 private
  • 不知道构造函数参数修饰符语法

延伸追问

  • private#private 在 JS/TS 中有什么区别?
  • 抽象类中的 abstract 方法和 protected 有什么关系?

口头回答版

| 修饰符 | 作用 | |--------|------| | public | 默认修饰符,任何地方都可访问 | | private | 只能在类内部访问(编译期检查,运行时可通过如 # 以外的手段访问) |


FB-02-SC-A-001:如何用 TypeScript 设计一个可辨识联合(Discriminated Union)处理业务状态?

题型:场景设计题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:discriminated union、可辨识联合、状态机、类型收窄 出现频率:高频 预计回答时长:5-10 分钟

题目描述: 假设有一个异步加载状态的业务场景,包含 idle、loading、success、error 四种状态。请用 TypeScript 设计类型,使得在每种状态下都能安全访问对应的数据。

参考答案

使用可辨识联合,通过共有字段 status 区分不同状态:

ts
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function render<T>(state: AsyncState<T>) {
  switch (state.status) {
    case 'idle':
      return 'Ready to load';
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Data: ${state.data}`; // 安全访问 data
    case 'error':
      return `Error: ${state.error.message}`; // 安全访问 error
    default:
      // 穷尽性检查
      const _exhaustive: never = state;
      return _exhaustive;
  }
}

设计要点:

  1. 每个分支有唯一的字面量 tag(如 status)。
  2. 分支专属数据只在该分支中存在。
  3. 配合 switchif 做类型收窄。
  4. 使用 never 做穷尽性检查,防止遗漏状态。

评分维度

  • 正确定义可辨识联合(40%)
  • 分支数据隔离设计合理(30%)
  • 使用 switch 收窄并访问专属数据(20%)
  • 使用 never 穷尽性检查(10%)

常见错误

  • 所有数据都放在一个类型里,导致可选属性泛滥
  • 不使用字面量 tag,无法用 switch 收窄
  • 忘记做穷尽性检查

延伸追问

  • 如果新增一种状态但没有更新 switch,TypeScript 会如何提示?
  • 可辨识联合和 Redux action 设计有什么关系?

参考资源

口头回答版

使用可辨识联合,通过共有字段 status 区分不同状态: 每个分支有唯一的字面量 tag(如 status)。 分支专属数据只在该分支中存在。 配合 switch 或 if 做类型收窄。


FB-02-CD-A-003:手写一个 DeepReadonly 类型

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:递归类型、DeepReadonly、映射类型、工具类型 出现频率:中频 预计回答时长:5-10 分钟

题目描述: 请实现一个 DeepReadonly<T> 类型,使对象的所有层级属性都变为只读,包括数组和嵌套对象。

参考答案

ts
type DeepReadonly<T> = T extends (infer R)[]
  ? ReadonlyArray<DeepReadonly<R>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

// 使用
interface User {
  name: string;
  address: {
    city: string;
  };
  tags: string[];
}

type ReadonlyUser = DeepReadonly<User>;

const user: ReadonlyUser = {
  name: 'Tom',
  address: { city: 'Beijing' },
  tags: ['admin']
};

// user.address.city = 'Shanghai'; // Error
// user.tags.push('guest');        // Error

要点:

  • 使用条件类型判断是否为数组,避免数组方法被递归为对象键。
  • 使用映射类型递归处理对象属性。
  • 基本类型保持不变。

评分维度

  • 正确处理对象属性递归(40%)
  • 正确处理数组类型(30%)
  • 基本类型保持不变(20%)
  • 能写出使用示例(10%)

常见错误

  • 直接用 Readonly<T>,只处理第一层
  • 数组被当成普通对象处理,导致 length 等属性也递归
  • 没有处理函数、Map、Set 等特殊类型

延伸追问

  • 如何处理函数类型,使其也被 DeepReadonly 忽略?
  • 如果 T 是 Promise 或 Map,应该如何处理?

口头回答版

  • 使用条件类型判断是否为数组,避免数组方法被递归为对象键。 - 使用映射类型递归处理对象属性。 - 基本类型保持不变。

FB-02-CO-A-010:TypeScript 的模块解析策略有哪些?

题型:概念题 难度:🟡 进阶 岗位层级:高级 面试知识域:02 TypeScript 标签:module resolution、模块解析、classic、node、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的模块解析策略,并说明 classicnode 两种策略的区别。

参考答案

模块解析是指 TypeScript 编译器如何根据 import / export 语句找到对应的模块文件。TS 提供两种解析策略:

  1. Classic(经典策略)

    • TS 1.6 之前的默认策略。
    • 对于相对路径导入,只查找 .ts.d.ts.tsx 文件。
    • 对于非相对路径导入,从当前目录向上查找。
    • 不识别 node_modules
  2. Node(Node.js 策略,现代默认)

    • 模拟 Node.js 的模块解析算法。
    • 相对路径导入会查找 .tsx.ts.d.ts.jsx.js(按配置顺序)。
    • 非相对路径导入会从 node_modules 中查找。
    • 支持 package.json 中的 typesmainexports 字段。
json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

相关配置:

  • baseUrl:非相对模块解析的基地址。
  • paths:路径映射,用于别名解析。
  • rootDirs:将多个目录视为一个根目录。
  • typeRoots / types:控制 @types 包的加载。

评分维度

  • 解释两种解析策略(40%)
  • 说明 Node 策略的查找过程(30%)
  • 提到 paths / baseUrl 配置(20%)
  • 说明 modern bundler 与 tsc 解析的差异(10%)

常见错误

  • 不知道 classic 和 node 的区别
  • 配置了 paths 但忘记配置 baseUrl
  • 混淆运行时模块解析和编译期模块解析

延伸追问

  • Vite/Webpack 中的路径别名与 tsconfig paths 有什么关系?
  • exports 字段如何影响 TypeScript 的模块解析?

参考资源

口头回答版

模块解析是指 TypeScript 编译器如何根据 import / export 语句找到对应的模块文件。 TS 提供两种解析策略: Classic(经典策略) - TS 1.6 之前的默认策略。


FB-02-CO-A-014:函数重载的使用场景和实现方式是什么?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:函数重载、overload、类型签名 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请用函数重载为同一个函数设计多个调用签名。

参考答案

函数重载允许为同一函数提供多个调用签名,编译器根据参数数量和类型选择匹配的签名。实现签名的参数类型和返回类型必须兼容所有重载签名。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  return a + b;
}

评分维度

  • 签名设计(40%):多个调用签名
  • 实现兼容(30%):实现签名兼容所有重载
  • 场景(30%):根据参数类型返回不同结果

常见错误

  • 重载签名没有实现签名
  • 实现签名参数类型比重载更窄
  • 在可以用联合类型和泛型解决的场景滥用重载

口头回答版

函数重载通过多个调用签名描述不同参数组合,实现签名必须兼容所有签名,适合参数类型决定返回类型的场景。

FB-02-CO-A-015:索引签名与 Record 类型有什么区别?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:索引签名、Record、映射类型、键类型 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请比较 [key: string]: TRecord<string, T> 的用法和约束差异。

参考答案

索引签名允许对象拥有任意数量的某类型属性。Record<string, T> 是内置映射类型,表示键为 string、值为 T 的对象。Record 更简洁,但键类型可以是联合类型;索引签名只能使用 string、number 或 symbol。

interface A { [key: string]: number; }
type B = Record&lt;string, number&gt;;
type C = Record<'a'|'b', number>;

评分维度

  • 定义方式(30%):接口索引签名 vs Record
  • 键类型(40%):Record 支持字面量联合,索引签名只接受 string/number/symbol
  • 使用场景(30%):动态属性 vs 固定键集合

常见错误

  • 认为 Record 不支持字面量键联合
  • 在需要已知键集合时仍用索引签名,丢失精确类型
  • 索引签名中键名为保留字导致意外问题

口头回答版

索引签名描述任意动态属性;Record 更简洁,支持字面量联合键,适合固定键集合。

FB-02-CO-A-016:什么是模板字面量类型?举一个实际例子。

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:模板字面量类型、字符串类型、类型体操 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释模板字面量类型如何构造字符串联合类型。

参考答案

模板字面量类型用反引号语法组合字符串字面量类型,可生成新的字符串联合类型。常用于事件名、CSS 变量名、路由路径等。

type EventName&lt;T extends string&gt; = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"

评分维度

  • 语法(40%):反引号组合类型
  • 生成联合(30%):多个占位符生成笛卡尔积
  • 场景(30%):事件名、路由、CSS 变量

常见错误

  • 把模板字面量类型和普通字符串模板混淆
  • 在不需要精确字符串类型的场景过度使用
  • 不理解 Capitalize、Uppercase 等内置字符串工具类型

口头回答版

模板字面量类型通过反引号组合字面量类型生成字符串联合,适合事件名、路由等场景。

FB-02-CO-A-017:什么是声明合并与模块增强?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:声明合并、模块增强、interface、declare 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 Declaration Merging 和 Module Augmentation 的使用场景。

参考答案

声明合并指同名的 interface、namespace、enum 等会被合并为一个定义,常用于扩展全局类型。模块增强用于为第三方模块补充类型声明,如给 Vue 的 ComponentCustomProperties 增加属性。

declare module 'vue' {
  interface ComponentCustomProperties {
    $api: ApiClient;
  }
}

评分维度

  • 声明合并(40%):同名 interface/namespace 合并
  • 模块增强(40%):declare module 扩展第三方类型
  • 场景(20%):插件属性、全局类型扩展

常见错误

  • 用 type 做声明合并
  • 模块增强没有放在正确的 .d.ts 文件导致不生效
  • 混淆 declare module 与 import 的语法

口头回答版

声明合并让同名声明自动合并;模块增强通过 declare module 为第三方库扩展类型。

FB-02-CO-A-018:枚举 Enum 有哪些缺陷?可以用什么替代?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:Enum、联合类型、const assertion、类型安全 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请说明 enum 的编译产物、类型安全问题和替代方案。

参考答案

enum 会生成运行时对象和反向映射,增加打包体积;常量枚举 const enum 有内联限制和编译兼容性风险。替代方案:使用联合类型或 as const 对象,配合 satisfies 获得更好的类型推断和 tree-shaking。

const Status = {
  Pending: 'pending',
  Done: 'done',
} as const;
type Status = typeof Status[keyof typeof Status];

评分维度

  • 编译产物(30%):运行时对象和反向映射
  • 体积(20%):增加打包体积
  • 替代(30%):as const 联合类型
  • const enum 风险(20%):isolatedModules 报错

常见错误

  • 在所有场景使用 enum 而不考虑打包体积
  • const enum 跨包导出导致 isolatedModules 报错
  • 用字符串枚举时混淆运行时值和类型名

口头回答版

enum 会增加运行时对象和体积,可用 as const 对象加联合类型替代,以获得更好推断和 tree-shaking。

FB-02-CO-A-019:字面量类型与联合类型如何结合使用?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:字面量类型、联合类型、类型收窄、枚举 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请用字面量联合类型定义状态和事件名,并说明类型收窄优势。

参考答案

字面量类型表示具体的字符串、数字或布尔值;联合类型将多个字面量组合。配合 switch 或 if 可实现穷尽检查,避免拼写错误。

type Status = 'idle' | 'loading' | 'success' | 'error';
function handle(s: Status) {
  switch (s) { case 'idle': ... }
}

评分维度

  • 字面量类型(30%):具体值作为类型
  • 联合类型(30%):组合多个字面量
  • 类型收窄(40%):switch 穷尽检查

常见错误

  • 用 string 代替字面量联合,丢失类型检查
  • 联合类型成员过多导致类型提示困难
  • switch 没有 default,编译器无法穷尽检查

口头回答版

字面量联合类型用于状态和事件名,可在 switch 中实现穷尽检查,减少拼写错误。

FB-02-CO-A-020:TypeScript 中的类型保护(Type Guards)有哪些?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:Type Guards、typeof、instanceof、自定义保护 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请列举内置和自定义类型保护,并说明它们如何收窄类型。

参考答案

内置:typeof 收窄基本类型,instanceof 收窄类实例,in 检查属性,Array.isArray。自定义:函数返回 value is Type 谓词,配合控制流分析收窄。

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

评分维度

  • 内置保护(50%):typeof/instanceof/in/Array.isArray
  • 自定义保护(30%):value is Type
  • 收窄效果(20%):控制流分析

常见错误

  • 自定义类型保护函数没有返回类型谓词
  • 用 as 强制断言替代类型保护
  • 对联合类型成员使用 typeof 无法区分的场景未用自定义保护

口头回答版

类型保护通过 typeof、instanceof、in、自定义谓词函数收窄联合类型,避免使用 as 断言。

FB-02-CO-A-021:泛型默认值如何与条件类型结合使用?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:泛型默认值、条件类型、infer、类型工具 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请写一个带默认类型参数的泛型工具,并解释其推导过程。

参考答案

泛型默认值在调用方未传入类型时使用预设类型。与条件类型结合可创建根据输入类型推断输出类型的工具。

type MaybeArray&lt;T = any&gt; = T extends any[] ? T : T[];
type A = MaybeArray&lt;string&gt;; // string[]

评分维度

  • 默认值(40%):未传类型时的回退
  • 条件类型(40%):extends 分支选择
  • 组合效果(20%):推导输出类型

常见错误

  • 默认值使用 any 导致类型安全丢失
  • 条件类型分支没有覆盖所有输入情况
  • 混淆默认值与类型约束 extends

口头回答版

泛型默认值与条件类型结合,可在未传类型时回退默认值,并按条件推导输出类型。


FB-02-CO-A-022:TypeScript 中类型断言 as 与尖括号语法有什么区别?

题型:概念题 难度:🟡 进阶 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型断言、as、尖括号、JSX 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请说明两种语法形式的差异及在 JSX 中为什么推荐用 as。

参考答案

as 语法:value as Type。尖括号语法:<Type>value。两者功能相同,但在 TSX/JSX 中尖括号会与 JSX 标签冲突,因此必须使用 as。

const len = (someValue as string).length;
const len2 = (&lt;string&gt;someValue).length; // 非 JSX 可用

评分维度

  • 语法差异(40%):as vs 尖括号
  • JSX 限制(40%):尖括号冲突
  • 推荐做法(20%):统一使用 as

常见错误

  • 在 TSX 中使用尖括号导致解析错误
  • 把类型断言当成类型转换在运行时生效
  • 用 as 绕过类型检查而不加注释说明

口头回答版

as 和尖括号类型断言功能相同,TSX 中尖括号与 JSX 冲突,推荐统一使用 as。


深入题(19 道)

口头回答版

模块解析是指 TypeScript 编译器如何根据 import / export 语句找到对应的模块文件。 TS 提供两种解析策略: Classic(经典策略) - TS 1.6 之前的默认策略。

FB-02-CO-P-001:infer 关键字的作用是什么?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:infer、条件类型、类型提取、类型推断、类型编程 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中 infer 关键字的作用,并举例说明如何使用它从复杂类型中提取子类型。

参考答案

infer 关键字用于在条件类型中声明一个类型变量,让 TypeScript 从待推断的类型中提取某一部分。

ts
// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;

type A = ElementType<number[]>; // number
type B = ElementType<string>;   // never

// 提取函数返回类型
type Return<T> = T extends (...args: any[]) => infer R ? R : never;

function fn() {
  return { a: 1 };
}

type R = Return<typeof fn>; // { a: number }

// 提取 Promise 的泛型参数
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type C = UnwrapPromise<Promise<string>>; // string

infer 只能在条件类型的 extends 子句中使用,不能在其他地方声明类型变量。

评分维度

  • 解释 infer 的基本作用(40%)
  • 举例提取数组/函数/Promise 类型(40%)
  • 说明 infer 的使用位置限制(20%)

常见错误

  • 在条件类型外使用 infer
  • 不理解 infer 只能推断一个候选类型
  • 不会处理 infer 失败的情况

延伸追问

  • 如何从函数参数类型中提取第二个参数类型?
  • infer 在多个候选类型时会如何选择?

相关题目

口头回答版

infer 关键字用于在条件类型中声明一个类型变量,让 TypeScript 从待推断的类型中提取某一部分。 infer 只能在条件类型的 extends 子句中使用,不能在其他地方声明类型变量。


FB-02-CO-P-002:什么是 Template Literal Types(模板字面量类型)?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:template literal types、模板字面量类型、字符串操作、类型编程 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的模板字面量类型,并举例说明其应用场景。

参考答案

模板字面量类型允许使用字符串模板在类型层面构造新的字符串类型,语法与 JS 模板字符串类似。

ts
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>; // 'onClick'
type HoverEvent = EventName<'hover'>; // 'onHover'

应用场景:

  1. 生成事件名

    ts
    type Events = 'click' | 'change';
    type Handlers = `on${Capitalize<Events>}`; // 'onClick' | 'onChange'
  2. CSS 变量名

    ts
    type ThemeKey = 'primary' | 'secondary';
    type CSSVar = `--color-${ThemeKey}`; // '--color-primary' | '--color-secondary'
  3. 对象键转换

    ts
    type Getters<T> = {
      [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
    };

模板字面量类型常与 as 重映射、条件类型、infer 结合使用。

评分维度

  • 解释模板字面量类型语法(40%)
  • 举例说明应用场景(40%)
  • 提到与映射类型 as 重映射的结合(20%)

常见错误

  • 把模板字面量类型等同于运行时模板字符串
  • 不了解 CapitalizeUppercase 等内置字符串操作类型
  • 在模板中错误拼接非 string 类型

延伸追问

  • 如何从模板字面量类型中提取变量部分?
  • 模板字面量类型在大规模联合类型上会有什么性能问题?

参考资源

口头回答版

模板字面量类型允许使用字符串模板在类型层面构造新的字符串类型,语法与 JS 模板字符串类似。 模板字面量类型常与 as 重映射、条件类型、infer 结合使用。


FB-02-CD-P-001:手写一个类型安全的 Promise.all

题型:手写代码题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:泛型、tuple、Promise、类型推断、递归类型 出现频率:中频 预计回答时长:5-10 分钟

题目描述: 请实现一个类型安全的 all 函数,接收一个 Promise 数组或元组,返回的 Promise 解析为对应的结果数组或元组类型。

参考答案

ts
type PromiseTuple<T extends readonly unknown[]> = {
  -readonly [P in keyof T]: Promise<Awaited<T[P]>> | T[P];
};

async function all<T extends readonly unknown[]>(
  values: PromiseTuple<T>
): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }> {
  return Promise.all(values) as any;
}

// 使用
const result = await all([
  Promise.resolve(1),
  Promise.resolve('hello'),
  Promise.resolve(true)
]);
// result 类型为 [number, string, boolean]

const names = await all(['a', 'b']); // string[]

要点:

  • 使用泛型 T extends readonly unknown[] 捕获输入元组类型。
  • 使用 Awaited<T[P]>(TS 4.5+)递归解开 Promise 嵌套。
  • 输出保持元组结构。

评分维度

  • 正确捕获输入元组类型(30%)
  • 正确推断输出元组类型(30%)
  • 使用 Awaited 处理 Promise 嵌套(25%)
  • 处理数组和元组两种情况(15%)

常见错误

  • 返回类型写成 Promise<T[]>,丢失元组结构
  • 不处理 Promise 中的 Promise(嵌套 Promise)
  • 使用 any 作为返回类型

延伸追问

  • 如果输入数组长度不固定,如何设计返回类型?
  • 如何处理输入中的非 Promise 值?

口头回答版

  • 使用泛型 T extends readonly unknown[] 捕获输入元组类型。 - 使用 Awaited<T[P]60>(TS 4.5+)递归解开 Promise 嵌套。 - 输出保持元组结构。

FB-02-CO-P-003:什么是 Recursive Types(递归类型)?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:recursive types、递归类型、树、JSON、深度类型 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的递归类型,并举例说明如何使用递归类型定义树形结构或 JSON 类型。

参考答案

递归类型是指在类型定义中引用自身的类型,用于表达嵌套、树形或自相似结构。

ts
// 树节点
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

const tree: TreeNode<number> = {
  value: 1,
  children: [
    { value: 2, children: [] },
    { value: 3, children: [{ value: 4, children: [] }] }
  ]
};

// JSON 类型
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const data: JSONValue = {
  name: 'Tom',
  scores: [90, 85],
  meta: { active: true }
};

注意:

  • TS 4.1 之前对递归类型有较多限制,现在已大幅改善。
  • 递归类型过深可能导致类型实例化过深错误(Type instantiation is excessively deep)。

评分维度

  • 解释递归类型的概念(40%)
  • 举例定义树形结构(30%)
  • 举例定义 JSON 类型(20%)
  • 提到递归深度的限制(10%)

常见错误

  • 递归类型没有终止条件
  • 在递归映射类型中导致无限递归
  • 不了解 TS 版本对递归类型的支持差异

延伸追问

  • 递归类型在编译时有什么性能风险?
  • 如何用递归类型实现 DeepPartial?

相关题目

口头回答版

递归类型是指在类型定义中引用自身的类型,用于表达嵌套、树形或自相似结构。 - TS 4.1 之前对递归类型有较多限制,现在已大幅改善。 - 递归类型过深可能导致类型实例化过深错误(Type instantiation is excessively deep)。


FB-02-CO-P-004:什么是 Branded Types(品牌类型)?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:branded types、品牌类型、名义类型、类型安全、newtype 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的 Branded Types,并说明它与基于结构类型的普通类型有什么区别。举例说明其应用场景。

参考答案

TypeScript 默认是结构类型系统(structural typing),只要结构相同就视为兼容。Branded Types 通过在类型中添加一个不可见的标记属性,模拟名义类型(nominal typing),使相同结构但语义不同的类型不兼容。

ts
type UserID = string & { __brand: 'UserID' };
type OrderID = string & { __brand: 'OrderID' };

function createUserID(id: string): UserID {
  return id as UserID;
}

function queryUser(id: UserID) {
  // ...
}

const userId = createUserID('123');
const orderId = '123' as OrderID;

queryUser(userId); // OK
// queryUser(orderId); // Error: OrderID 不能赋值给 UserID

应用场景:

  • 区分不同业务含义的 ID(UserID、OrderID、ProductID)。
  • 区分不同单位(米、厘米、像素)。
  • 防止不同语义但结构相同的值被混用。

评分维度

  • 解释结构类型与名义类型的区别(30%)
  • 说明 Branded Types 的实现原理(35%)
  • 举例应用场景(25%)
  • 提到运行时 brand 不存在(10%)

常见错误

  • 认为 brand 属性在运行时存在
  • 直接 as UserID 而不通过工厂函数
  • 把 brand 类型设计得过于复杂

延伸追问

  • Branded Types 和私有构造函数类有什么区别?
  • 如何在 JSON 序列化中保留 Branded Types 的语义?

参考资源

口头回答版

TypeScript 默认是结构类型系统(structural typing),只要结构相同就视为兼容。 Branded Types 通过在类型中添加一个不可见的标记属性,模拟名义类型(nominal typing),使相同结构但语义不同的类型不兼容。 - 区分不同业务含义的 ID(UserID、OrderID、ProductID)。 - 区分不同单位(米、厘米、像素)。


FB-02-CO-P-005:什么是 Declaration Merging(声明合并)?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:declaration merging、声明合并、interface、namespace、扩展 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的声明合并,并举例说明 interface、namespace 和 enum 的合并行为。

参考答案

声明合并是指 TypeScript 编译器将多个同名声明合并为一个声明的特性,主要用于扩展类型定义。

ts
// interface 合并
interface User {
  name: string;
}
interface User {
  age: number;
}

const user: User = { name: 'Tom', age: 20 }; // 合并为 { name: string; age: number }

// namespace 与函数合并
function greet(name: string) {
  console.log(`Hello, ${name}`);
}
namespace greet {
  export let version = '1.0';
}

greet('Tom');
console.log(greet.version);

// namespace 与 class 合并
class Album {
  label!: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel {
    name!: string;
  }
}

注意:

  • 只有 interfacenamespaceenum 支持声明合并。
  • type 别名不支持声明合并。
  • 合并的成员必须是唯一且不冲突的。

评分维度

  • 解释声明合并概念(30%)
  • 举例 interface 合并(30%)
  • 举例 namespace 与函数/class 合并(30%)
  • 说明 type 不支持合并(10%)

常见错误

  • 试图合并 type 别名
  • 合并 interface 时出现同名但不同类型的属性
  • 不了解声明合并的实际应用场景

延伸追问

  • 声明合并和模块增强(Module Augmentation)有什么关系?
  • 为什么第三方库扩展全局类型时常用 interface 合并?

相关题目

口头回答版

声明合并是指 TypeScript 编译器将多个同名声明合并为一个声明的特性,主要用于扩展类型定义。 - 只有 interface、namespace、enum 支持声明合并。 - type 别名不支持声明合并。 - 合并的成员必须是唯一且不冲突的。


FB-02-CO-P-006:什么是 Module Augmentation(模块增强)?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:module augmentation、模块增强、declare module、扩展第三方库 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中的模块增强,并说明如何为第三方库扩展类型定义。

参考答案

模块增强用于在不修改第三方库源码的情况下,扩展其导出的类型或模块声明。

ts
// 为 axios 扩展自定义 config 字段
declare module 'axios' {
  export interface AxiosRequestConfig {
    customHeader?: string;
    retryCount?: number;
  }
}

// 之后使用时
transport.get('/api', { customHeader: 'xxx', retryCount: 3 });

要点:

  • 使用 declare module 'module-name' { ... }
  • 内部使用 interface 进行声明合并,扩展原有类型。
  • 模块增强文件需要被 TS 编译器包含(在 tsconfig 的 include 中)。

常见场景:

  • 为 Vue 的 ComponentCustomProperties 添加全局属性。
  • 为 Express 的 Request 添加自定义字段。
  • 为 styled-components 扩展 theme 类型。

评分维度

  • 解释模块增强概念(30%)
  • 正确写出 declare module 语法(30%)
  • 举例说明扩展第三方类型(30%)
  • 说明文件需要被包含在编译中(10%)

常见错误

  • 使用 type 而不是 interface 进行扩展
  • 模块增强文件未被 tsconfig 包含
  • 在 declare module 中重复定义原有属性导致冲突

延伸追问

  • 模块增强和全局类型声明(declare global)有什么区别?
  • 如何为第三方库新增一个全新的导出?

参考资源

口头回答版

模块增强用于在不修改第三方库源码的情况下,扩展其导出的类型或模块声明。 - 使用 declare module 'module-name' { ... }。 - 内部使用 interface 进行声明合并,扩展原有类型。 - 模块增强文件需要被 TS 编译器包含(在 tsconfig 的 include 中)。


FB-02-CO-P-007:什么是 Variance(型变)?请解释协变、逆变、双变和不变。

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:variance、协变、逆变、双变、不变、类型兼容性 出现频率:中频 预计回答时长:5-10 分钟

题目描述: 请解释 TypeScript 中的型变(Variance)概念,包括协变(Covariance)、逆变(Contravariance)、双变(Bivariance)和不变(Invariance),并举例说明。

参考答案

型变描述的是复合类型(如函数参数、返回值、数组元素)与子类型关系之间的方向。

Dog extends Animal

型变含义TypeScript 行为示例
协变 Covariant子类型关系方向相同返回值、对象属性、数组元素Dog[]Animal[] 的子类型
逆变 Contravariant子类型关系方向相反函数参数(strictFunctionTypes 开启时)(animal: Animal) => void(dog: Dog) => void 的子类型
双变 Bivariant同时允许协变和逆变函数参数(strictFunctionTypes 关闭时)兼容性更宽松
不变 Invariant只允许完全相同类型可变数据结构、泛型参数Array<Dog>Array<Animal> 在不安全操作时不兼容
ts
interface Animal { name: string }
interface Dog extends Animal { breed: string }

// 协变:返回值
type GetAnimal = () => Animal;
type GetDog = () => Dog;
const getDog: GetDog = () => ({ name: 'Tom', breed: 'Husky' });
const getAnimal: GetAnimal = getDog; // OK,返回值协变

// 逆变:参数(strictFunctionTypes 下)
type FeedAnimal = (animal: Animal) => void;
type FeedDog = (dog: Dog) => void;
const feedAnimal: FeedAnimal = (a) => console.log(a.name);
const feedDog: FeedDog = feedAnimal; // OK,参数逆变
// feedAnimal = feedDog; // Error

评分维度

  • 准确解释四种型变概念(50%)
  • 能举例说明协变和逆变(30%)
  • 提到 strictFunctionTypes 的影响(20%)

常见错误

  • 混淆协变和逆变的方向
  • 认为 TypeScript 中所有地方都是协变
  • 不理解为什么函数参数是逆变

延伸追问

  • strictFunctionTypes 关闭时为什么会有双变?这有什么风险?
  • TypeScript 中泛型参数默认是什么型变?

参考资源

口头回答版

型变描述的是复合类型(如函数参数、返回值、数组元素)与子类型关系之间的方向。 设 Dog extends Animal: | 型变 | 含义 | TypeScript 行为 | 示例 | |------|------|-----------------|------|


FB-02-CO-P-008:TypeScript 的类型兼容性是基于名义类型还是结构类型?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:structural typing、类型兼容性、名义类型、duck typing 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 的类型兼容性是基于结构类型(Structural Typing)还是名义类型(Nominal Typing),并说明其优缺点。

参考答案

TypeScript 主要基于结构类型(structural typing),即类型的兼容性由结构(属性和方法)决定,而不是由声明名称决定。

ts
interface Point2D {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

const p3d: Point3D = { x: 1, y: 2, z: 3 };
const p2d: Point2D = p3d; // OK,Point3D 结构满足 Point2D

优点:

  1. 灵活性高,便于接口替换和 mocking。
  2. 更适合 JavaScript 的动态特性。
  3. 代码复用和测试更方便。

缺点:

  1. 可能意外兼容语义不同的类型。
  2. 需要 Branded Types 等技巧模拟名义类型。
  3. 大型重构时,同名但不同义的类型可能混用。

例外:

  • 类中的 private/protected 成员会让类型带有名义类型特征。
  • enum 在不同模块中同名但被视为不同。

评分维度

  • 说清结构类型定义(40%)
  • 举例说明兼容性判断(30%)
  • 说明优缺点(20%)
  • 提到例外情况(10%)

常见错误

  • 认为 TypeScript 是名义类型系统
  • 不理解结构类型与 duck typing 的关系
  • 忽略 private/protected 的名义类型特征

延伸追问

  • 如何用 TypeScript 实现名义类型效果?
  • 结构类型在跨模块共享接口时有什么风险?

相关题目

口头回答版

TypeScript 主要基于结构类型(structural typing),即类型的兼容性由结构(属性和方法)决定,而不是由声明名称决定。 灵活性高,便于接口替换和 mocking。 更适合 JavaScript 的动态特性。 代码复用和测试更方便。


FB-02-CO-P-009:tsconfig.json 中的 strict 模式包含哪些选项?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:tsconfig、strict、严格模式、编译选项、类型安全 出现频率:高频 预计回答时长:5-8 分钟

题目描述: 请说明 tsconfig.jsonstrict: true 会开启哪些编译选项,并解释其中几个关键选项的作用。

参考答案

strict: true 是严格模式的总开关,会开启以下选项:

选项作用
noImplicitAny禁止隐式 any,未注解的变量/参数必须能推断出类型
strictNullChecks开启后,null 和 undefined 不能赋值给其他类型
strictFunctionTypes函数参数使用逆变检查,方法参数使用双变
strictBindCallApplybind/call/apply 必须有正确的类型
strictPropertyInitialization类属性必须在声明或构造函数中初始化
noImplicitThis禁止 this 为隐式 any
alwaysStrict在编译结果中开启 "use strict"
useUnknownInCatchVariablescatch 块中的变量类型为 unknown 而不是 any
json
{
  "compilerOptions": {
    "strict": true
  }
}

建议:

  • 新项目始终开启 strict: true
  • 老项目迁移时可逐项开启,逐步修复类型问题。

评分维度

  • 列出 strict 包含的主要选项(40%)
  • 解释 noImplicitAny、strictNullChecks、strictFunctionTypes(40%)
  • 说明严格模式的工程价值(20%)

常见错误

  • 只开启 strict 但不了解具体子选项
  • 认为 strict 会降低开发效率而完全关闭
  • 混淆 strictNullChecksnoImplicitReturns

延伸追问

  • 老项目从非严格模式迁移到严格模式有哪些策略?
  • strictFunctionTypes 对方法声明和函数声明的处理为什么不同?

参考资源

口头回答版

strict: true 是严格模式的总开关,会开启以下选项: | 选项 | 作用 | |------|------| | noImplicitAny | 禁止隐式 any,未注解的变量/参数必须能推断出类型 |


FB-02-CD-P-002:手写一个类型安全的事件总线(Event Bus)

题型:手写代码题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:event bus、类型安全、泛型、事件系统、键值映射 出现频率:中频 预计回答时长:8-15 分钟

题目描述: 请用 TypeScript 实现一个类型安全的事件总线,支持根据事件名约束监听回调的参数类型和触发时的参数类型。

参考答案

ts
interface EventMap {
  login: { userId: number; username: string };
  logout: { userId: number };
  message: { content: string; timestamp: number };
}

class EventBus<Events extends Record<string, any>> {
  private listeners: {
    [K in keyof Events]?: Array<(payload: Events[K]) => void>;
  } = {};

  on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
    return () => this.off(event, listener);
  }

  off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void) {
    const list = this.listeners[event];
    if (list) {
      this.listeners[event] = list.filter((l) => l !== listener) as any;
    }
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]) {
    const list = this.listeners[event];
    if (list) {
      list.forEach((listener) => listener(payload));
    }
  }
}

const bus = new EventBus<EventMap>();

bus.on('login', ({ userId, username }) => {
  console.log(userId, username);
});

// bus.emit('login', { userId: 1 }); // Error: 缺少 username
// bus.on('message', (payload: string) => {}); // Error: 参数类型不匹配

要点:

  • 使用泛型 Events 描述事件名到负载类型的映射。
  • on/off/emit 都通过 keyof Events 约束事件名。
  • 监听器参数类型根据事件名自动推断。

评分维度

  • 正确设计 EventMap 泛型(30%)
  • on/off/emit 类型约束正确(40%)
  • 能展示类型错误的示例(20%)
  • 内存泄漏与取消订阅处理(10%)

常见错误

  • 事件名参数类型为 string,失去类型安全
  • 监听器参数类型为 any
  • 不处理重复订阅或取消订阅

延伸追问

  • 如何支持通配符事件监听(如 *)?
  • 如何实现一次订阅(once)?

口头回答版

  • 使用泛型 Events 描述事件名到负载类型的映射。 - on/off/emit 都通过 keyof Events 约束事件名。 - 监听器参数类型根据事件名自动推断。

FB-02-CO-P-010:Declaration Files(.d.ts)的作用是什么?

题型:概念题 难度:🔴 深入 岗位层级:专家 面试知识域:02 TypeScript 标签:declaration files、d.ts、类型声明、类型定义、@types 出现频率:高频 预计回答时长:3-5 分钟

题目描述: 请解释 TypeScript 中 .d.ts 声明文件的作用,并说明它与 .ts 文件的区别。

参考答案

.d.ts 文件只包含类型声明,不包含运行时逻辑。它用于:

  1. 为没有类型的 JavaScript 库提供类型定义。
  2. 声明全局变量、函数、模块。
  3. 通过 declaration: true 为 TS 库生成发布用的类型声明。
ts
// math.d.ts
declare function add(a: number, b: number): number;
declare const PI: number;

.ts 文件的区别:

特性.ts.d.ts
包含运行时代码
可被编译为 JS
只提供类型信息
用于库类型发布间接(生成 .d.ts)直接

@types 包就是社区维护的 .d.ts 声明文件集合。

评分维度

  • 解释 .d.ts 作用(40%)
  • 区分 .ts 和 .d.ts(30%)
  • 说明声明文件的应用场景(20%)
  • 提到 @types(10%)

常见错误

  • 在 .d.ts 中写运行时逻辑
  • 不理解 declare 关键字的作用
  • 混淆 .d.ts 与 .ts 的编译行为

延伸追问

  • declare moduledeclare global 有什么区别?
  • 如何为一个 JS 库手写类型声明文件?

参考资源

口头回答版

.d.ts 文件只包含类型声明,不包含运行时逻辑。 为没有类型的 JavaScript 库提供类型定义。 声明全局变量、函数、模块。 通过 declaration: true 为 TS 库生成发布用的类型声明。


FB-02-CO-P-015:TypeScript 的类型兼容性是基于结构类型还是名义类型?

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:类型兼容性、结构类型、名义类型、brand 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请解释结构类型系统的含义,以及如何实现名义类型效果。

参考答案

TypeScript 主要基于结构类型:只要结构兼容即可赋值,不要求显式继承或实现。这与 Java/C# 的名义类型不同。若需要名义类型效果,可用 Branded Type 或私有字段。

type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };

评分维度

  • 结构类型(40%):按结构而非名称兼容
  • 与名义类型对比(30%):Java/C# 的对比
  • 实现名义效果(30%):Brand 类型或私有字段

常见错误

  • 认为 TypeScript 是名义类型
  • 在需要区分同构类型时仅用 type alias
  • 滥用 any 绕过结构检查

口头回答版

TypeScript 基于结构类型,结构兼容即可赋值;需要名义类型效果可用 Branded Type。

FB-02-CO-P-016:TypeScript Compiler API 可以做什么?请举一个应用场景。

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:Compiler API、AST、类型检查、工具链 出现频率:低频 预计回答时长:5-8 分钟

题目描述: 请介绍 ts.createProgram、TypeChecker 等 API 的典型用途。

参考答案

Compiler API 允许程序访问 TypeScript 编译器的解析、类型检查、AST 遍历能力。可用于构建自定义 lint、自动生成 API 文档、类型检查 CI、代码迁移工具等。

import ts from 'typescript';
const program = ts.createProgram(['src/index.ts'], {});
const checker = program.getTypeChecker();

评分维度

  • 能力(40%):AST、类型检查、代码生成
  • 典型场景(40%):文档生成、lint、迁移
  • 限制(20%):学习曲线和版本兼容性

常见错误

  • 认为 Compiler API 可以修改运行时行为
  • 直接操作字符串而非 AST 做代码分析
  • 忽略 TypeScript 版本升级对 AST 节点的影响

口头回答版

TypeScript Compiler API 提供 AST 和类型检查能力,常用于自定义工具、文档生成和代码迁移。

FB-02-CO-P-017:什么是递归类型?使用递归类型要注意什么?

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:递归类型、JSON、类型系统 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请用递归类型定义一个 JSON 类型,并说明递归深度限制。

参考答案

递归类型指类型定义中引用自身,常用于树、JSON、链表等结构。TypeScript 支持接口自引用和条件类型递归,但过深递归会触发 TS2589 类型实例化过深。

type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };

评分维度

  • 自引用(40%):类型定义引用自身
  • 应用场景(30%):JSON、树形结构
  • 深度限制(30%):递归深度与编译错误

常见错误

  • 递归类型没有终止条件
  • 把运行时递归和类型递归混为一谈
  • 在复杂递归中不设置类型参数上限导致 TS2589

口头回答版

递归类型用于自引用结构如 JSON,需注意终止条件和递归深度限制。

FB-02-CO-P-018:制定一个从 JavaScript 迁移到 TypeScript 的方案

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:迁移、JavaScript、TypeScript、工程化 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 请描述迁移步骤、配置策略、风险控制和团队培训。

参考答案

步骤:1) 搭建 TS 编译配置和类型声明;2) 允许 JS/TS 混编,逐步迁移核心模块;3) 优先迁移公共库和接口层;4) 开启 strict 选项逐项修复;5) 建立类型检查 CI 门禁。

风险控制:使用 allowJs、noImplicitAny 关闭初期,使用 @ts-check 和 JSDoc 过渡,避免一次性全量重写。

评分维度

  • 渐进策略(40%):混编、按优先级迁移
  • 配置(30%):allowJs、strict 渐进开启
  • 团队(30%):培训、CI 门禁

常见错误

  • 一次性全量重写导致业务停滞
  • 直接开启 strict:true 产生大量无法短期修复的错误
  • 忽略第三方库类型补充导致 any 泛滥

口头回答版

迁移应渐进混编,优先核心模块,逐步开启 strict,并建立 CI 类型检查。

FB-02-CO-P-019:如何在类型安全与开发效率之间取得平衡?

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:类型安全、开发效率、any、工程化 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 请给出在大型项目中兼顾类型覆盖率和开发体验的实践。

参考答案

平衡策略:1) 核心库和公共 API 强制严格类型;2) 边界场景使用 unknown 而非 any;3) 允许临时 any 但需标注 TODO 和渐进修复;4) 使用类型推导减少冗余注解;5) 建立类型覆盖率指标,但不要追求 100% 而牺牲进度。

工具:zod 做运行时校验,ts-reset 改善内置类型。

评分维度

  • 核心严格(30%):公共 API 和库
  • any 管理(30%):用 unknown 替代,标注 TODO
  • 度量(20%):类型覆盖率而非 100%
  • 工具(20%):zod、ts-reset

常见错误

  • 所有代码强制 strict 导致迁移困难
  • 为赶进度大面积使用 any 不治理
  • 只看类型覆盖率,忽略关键路径的类型质量

口头回答版

核心路径严格类型,边界用 unknown,允许临时 any 并标注修复,配合运行时校验工具平衡安全与效率。

FB-02-CO-P-020:TypeScript 装饰器有哪些类型?其原理是什么?

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:装饰器、Decorator、元数据、实验性 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 请介绍类装饰器、方法装饰器、属性装饰器及其执行顺序。

参考答案

装饰器是特殊的函数,可在类、方法、访问器、属性、参数声明前使用。旧版实验性装饰器通过修改原型或属性描述符实现;TC39 新装饰器采用函数包装方式,不依赖 metadata。

执行顺序:参数 > 方法 > 访问器 > 属性 > 类,由近及远。

补充说明

在实际落地 TypeScript 装饰器有哪些类型其原理是什么 时,建议结合 装饰器、Decorator、元数据 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 装饰器类型(40%):类/方法/属性/参数装饰器
  • 原理(30%):描述符包装 vs 函数包装
  • 执行顺序(30%):由近及远、参数优先

常见错误

  • 把装饰器当成运行时 AOP 任意修改类
  • 混淆实验性装饰器和新版 TC39 装饰器 API
  • 忽略 reflect-metadata 对类型元数据的额外配置

口头回答版

TypeScript 装饰器分为类、方法、属性、参数装饰器,旧版基于描述符,新版基于函数包装,执行顺序由近及远。

FB-02-CO-P-021:类型体操的度在哪里?如何保持可维护性?

题型:概念题 难度:🔴 深入 岗位层级:高级 / 专家 面试知识域:02 TypeScript 标签:类型体操、可维护性、类型系统、工程化 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 请讨论复杂类型推导与代码可读性之间的平衡。

参考答案

类型体操适合库和框架内部抽象,业务代码应优先简单类型。保持可维护性:1) 命名中间类型;2) 注释复杂推导的意图;3) 单元测试类型;4) 控制递归深度;5) 团队成员能理解是上限。

过度类型体操会导致编译变慢、错误信息难以阅读和新人上手困难。

补充说明

在实际落地 类型体操的度在哪里如何保持可维护性 时,建议结合 类型体操、可维护性、类型系统 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 适用场景(40%):库框架 vs 业务代码
  • 命名与注释(30%):中间类型和意图
  • 测试(30%):类型级单元测试

常见错误

  • 在业务代码中追求极度复杂的类型
  • 类型推导没有命名中间结果
  • 错误信息无法阅读时不增加注释或简化

口头回答版

类型体操适合底层库,业务代码应简单;通过命名、注释、测试和限制复杂度保持可维护。


架构题(31 道)

口头回答版

.d.ts 文件只包含类型声明,不包含运行时逻辑。 为没有类型的 JavaScript 库提供类型定义。 声明全局变量、函数、模块。 通过 declaration: true 为 TS 库生成发布用的类型声明。

FB-02-PE-R-001:如何优化大型 TypeScript 项目的编译性能?

题型:性能优化题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:TypeScript、编译性能、tsconfig、增量编译、project references 出现频率:高频 预计回答时长:8-15 分钟

题目描述: 在一个大型 TypeScript 项目(如 Monorepo 或数十万行代码)中,编译速度变慢、IDE 卡顿、类型检查耗时。请给出系统性的优化方案。

参考答案

  1. 项目引用(Project References)

    • 将大项目拆分为多个子项目,通过 references 配置。
    • 每个子项目独立编译,支持增量构建。
    • 配合 composite: trueincremental: true
  2. 启用增量编译

    json
    {
      "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./.tsbuildinfo"
      }
    }
  3. 控制 include / exclude 范围

    • 避免包含 node_modules、测试文件、构建产物。
    • 使用 filesinclude 精确指定源文件。
  4. 减少类型复杂度

    • 避免过深的递归类型。
    • 避免生成巨大的联合类型(尤其是模板字面量类型)。
    • 谨慎使用 extends 泛型约束中的复杂条件类型。
  5. 使用 skipLibCheck: true

    • 跳过声明文件(.d.ts)的类型检查,显著提速。
  6. 按需加载类型

    • 对第三方库使用 @types 时只安装需要的。
    • 避免全局类型污染。
  7. 并行与缓存

    • 使用 tsc --build 利用项目引用。
    • 结合构建工具(如 esbuild、swc、turborepo)做并行与缓存。
  8. 监控与度量

    • 使用 tsc --extendedDiagnostics--generateTrace 分析耗时。
    • 关注 Check timeInstantiate time 等指标。

评分维度

  • 提到 Project References 与增量编译(25%)
  • 提到 skipLibCheck 与 include 优化(20%)
  • 提到减少类型复杂度(20%)
  • 提到构建工具与缓存策略(20%)
  • 提到性能度量方法(15%)

常见错误

  • 只建议升级硬件
  • 完全关闭类型检查换取速度
  • 不理解 Project References 的适用场景

延伸追问

  • --extendedDiagnostics 输出的各项指标分别代表什么?
  • 模板字面量类型为什么会影响编译性能?

参考资源

口头回答版

项目引用(Project References) - 将大项目拆分为多个子项目,通过 references 配置。 - 每个子项目独立编译,支持增量构建。 - 配合 composite: true 和 incremental: true。


FB-02-SD-R-001:如何为大型前端项目设计类型系统架构?

题型:系统设计题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:类型系统、架构设计、domain model、类型共享、类型安全 出现频率:高频 预计回答时长:10-20 分钟

题目描述: 请设计一个大型前端项目(如电商后台管理系统)的类型系统架构,要求类型清晰、可维护、可扩展,并能在前后端之间共享部分类型契约。

参考答案

  1. 领域模型层(Domain Model)

    • 定义核心业务实体:UserOrderProductPayment 等。
    • 使用 Branded Types 区分不同业务 ID。
    • 将领域模型与 DTO/API 类型分离。
  2. API 契约层

    • 定义请求参数、响应数据、错误码类型。
    • 通过 OpenAPI/Swagger 或 tRPC 自动生成前后端共享类型。
    • 使用 as const 定义 API endpoint 常量。
  3. 应用层类型

    • ViewModel、FormModel、RouteParams、State 等。
    • 通过工具类型从 Domain Model 推导,避免重复定义。
  4. 共享类型包

    • 在 Monorepo 中创建 @project/types@project/shared
    • 前后端共同引用,避免重复和维护不一致。
  5. 类型目录组织

    src/
    ├── types/
    │   ├── domain/        # 领域模型
    │   ├── api/           # API 契约
    │   ├── ui/            # 组件/视图类型
    │   ├── utils/         # 工具类型
    │   └── global.d.ts    # 全局声明
  6. 类型安全边界

    • 使用 unknown 解析外部数据。
    • 使用 zod/io-ts 做运行时校验。
    • 使用 satisfies 表达式约束对象字面量。
  7. 命名与约定

    • DTO 后缀表示传输对象:CreateOrderDTOOrderResponseDTO
    • 使用 *Params*Options*Config 等语义化命名。

评分维度

  • 分层设计清晰(30%)
  • 提到前后端类型共享(20%)
  • 提到 Branded Types / 运行时校验(20%)
  • 类型目录组织合理(15%)
  • 考虑可扩展性和维护性(15%)

常见错误

  • 所有类型堆在一个文件里
  • 直接用 API 响应类型作为视图模型
  • 忽略运行时校验,完全依赖编译期类型

延伸追问

  • 当后端接口返回字段变化时,如何保证前端类型同步?
  • 领域模型和 DTO 不一致时,应该在哪里做转换?

口头回答版

领域模型层(Domain Model) - 定义核心业务实体:User、Order、Product、Payment 等。 - 使用 Branded Types 区分不同业务 ID。 - 将领域模型与 DTO/API 类型分离。


FB-02-EN-R-001:Monorepo 中 TypeScript 配置应该如何设计?

题型:工程化题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:Monorepo、tsconfig、project references、共享配置、workspace 出现频率:高频 预计回答时长:10-15 分钟

题目描述: 在一个 Monorepo(如 pnpm workspace + Turborepo)中,多个前端应用和共享包共存。请设计一套 TypeScript 配置方案,保证类型一致性、构建效率和开发体验。

参考答案

  1. 共享基础配置

    • @project/tsconfig 或根目录创建 tsconfig.base.json
    • 各包通过 extends 继承。
    json
    {
      "extends": "@project/tsconfig/base.json",
      "compilerOptions": {
        "outDir": "./dist"
      }
    }
  2. 按包类型分层配置

    • tsconfig.app.json:前端应用(DOM、JSX)。
    • tsconfig.lib.json:共享库(declaration: true)。
    • tsconfig.node.json:Node 脚本/工具。
  3. 项目引用(Project References)

    json
    {
      "references": [
        { "path": "../shared-ui" },
        { "path": "../utils" }
      ]
    }
    • 被引用包开启 composite: true
    • 使用 tsc --build 进行增量编译。
  4. 路径别名与解析

    • 包内使用相对路径或 TypeScript paths。
    • 跨包使用包名导入(通过 workspace 协议)。
    • 避免循环依赖。
  5. 构建与类型检查分离

    • 开发/生产打包使用 Vite/Webpack/esbuild/swc(不依赖 tsc 输出)。
    • 类型检查单独运行 tsc --noEmit
  6. 一致的工具链

    • 统一 TypeScript 版本。
    • 使用 typesync 管理 @types。
    • CI 中运行全量类型检查。

评分维度

  • 共享基础配置设计(20%)
  • Project References 使用(25%)
  • 分层 tsconfig 设计(20%)
  • 构建与类型检查分离(20%)
  • 跨包依赖管理(15%)

常见错误

  • 每个包完全独立的 tsconfig,无法统一升级
  • 不使用 Project References 导致全量编译
  • 用 tsc 输出作为打包输入,拖慢构建

延伸追问

  • 如何解决不同包之间 TypeScript 版本不一致的问题?
  • 循环依赖在 Project References 中会导致什么问题?

口头回答版

  • 在 @project/tsconfig 或根目录创建 tsconfig.base.json。 - 各包通过 extends 继承。 - tsconfig.app.json:前端应用(DOM、JSX)。 - tsconfig.lib.json:共享库(declaration: true)。

FB-02-SD-R-002:如何设计类型安全的 API Client?

题型:系统设计题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:api client、类型安全、泛型、请求封装、错误处理 出现频率:高频 预计回答时长:10-20 分钟

题目描述: 请设计一个类型安全的 HTTP API Client,使得每个接口的路径、请求参数、响应数据和错误类型都能在编译期得到约束。

参考答案

  1. 定义 API 契约

    ts
    interface APIDefinition {
      '/users': {
        GET: {
          response: { id: number; name: string }[];
        };
      };
      '/users/:id': {
        GET: {
          params: { id: string };
          response: { id: number; name: string };
        };
        POST: {
          params: { id: string };
          body: { name: string };
          response: { id: number; name: string };
        };
      };
    }
  2. 泛型客户端实现

    ts
    class APIClient<T extends Record<string, Record<string, any>>> {
      request<
        Path extends keyof T,
        Method extends keyof T[Path]
      >(
        path: Path,
        method: Method,
        options?: T[Path][Method] extends { params: infer P } ? { params: P } : {}
      ): Promise<
        T[Path][Method] extends { response: infer R } ? R : never
      > {
        // 实际请求实现
        return fetch(path as string).then((r) => r.json()) as any;
      }
    }
  3. URL 参数替换

    • 使用模板字面量类型解析 :id
    • 确保所有路径参数都被提供。
  4. 错误类型

    • 为不同接口定义不同的错误响应。
    • 使用结果类型 Result<T, E> 或异常处理。
  5. 运行时校验

    • 使用 zod 等库对响应数据做 schema 校验。
    • 类型与运行时结构保持一致。

评分维度

  • API 契约设计清晰(25%)
  • 泛型约束路径/方法/参数/响应(30%)
  • 路径参数类型安全(20%)
  • 错误处理与运行时校验(15%)
  • 可扩展性(10%)

常见错误

  • 所有请求返回 Promise<any>
  • 路径参数用字符串拼接,不做类型约束
  • 忽略后端返回结构的运行时校验

延伸追问

  • 如何从 Swagger/OpenAPI 自动生成这种 API 契约?
  • 如果接口支持分页,类型应该如何设计?

口头回答版

  • 使用模板字面量类型解析 :id。 - 确保所有路径参数都被提供。 - 为不同接口定义不同的错误响应。 - 使用结果类型 Result<T, E70> 或异常处理。

FB-02-SD-R-003:如何在企业级应用中合理使用 Decorators?

题型:系统设计题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:decorators、装饰器、metadata、企业应用、AOP 出现频率:中频 预计回答时长:10-15 分钟

题目描述: TypeScript Decorators 可以用于类、方法、属性、参数等。请结合企业级应用场景,说明如何合理使用装饰器,以及需要注意哪些问题。

参考答案

  1. 常见应用场景

    • 依赖注入@Injectable()@Inject()
    • 路由/控制器@Controller()@Get()@Post()
    • 日志与监控@Log()@Metrics()
    • 权限校验@RequireAuth()@Permission()
    • 缓存@Cacheable()
    • ORM 映射@Entity()@Column()
  2. 实现示例

    ts
    function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const original = descriptor.value;
      descriptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyKey} with`, args);
        return original.apply(this, args);
      };
    }
    
    class UserService {
      @Log
      getUser(id: number) {
        return { id, name: 'Tom' };
      }
    }
  3. 注意事项

    • TS 5.0 开始支持 ECMAScript Decorators 标准,与旧版 experimentalDecorators 不完全兼容。
    • 装饰器执行顺序:参数 -> 方法 -> 属性 -> 类(由内而外、由下而上)。
    • 过度使用装饰器会增加代码隐式性,降低可读性。
    • 需要配合 reflect-metadata 使用元数据时,注意 polyfill 和打包体积。
  4. 设计建议

    • 优先用于横切关注点(AOP)。
    • 保持装饰器语义单一、可测试。
    • 文档化装饰器的运行时行为。

评分维度

  • 列举 3 个以上企业级场景(30%)
  • 解释装饰器执行顺序(20%)
  • 提到 TS 5.0 标准装饰器与旧版差异(15%)
  • 说明使用风险与边界(20%)
  • 给出设计建议(15%)

常见错误

  • 用装饰器处理所有逻辑,导致调试困难
  • 不理解装饰器在编译期和运行时的行为
  • 新旧装饰器 API 混用

延伸追问

  • 装饰器和高阶函数相比各有什么优劣?
  • 如何在不使用 reflect-metadata 的情况下实现依赖注入?

参考资源

口头回答版

  • 依赖注入:@Injectable()、@Inject()。 - 路由/控制器:@Controller()、@Get()、@Post()。 - 日志与监控:@Log()、@Metrics()。 - 权限校验:@RequireAuth()、@Permission()。

FB-02-SC-R-001:如何制定一个从 JavaScript 迁移到 TypeScript 的方案?

题型:场景设计题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:迁移、javascript、TypeScript、渐进式、strict 出现频率:高频 预计回答时长:10-20 分钟

题目描述: 一个已有 50 万行 JavaScript 代码的大型前端项目希望迁移到 TypeScript。请制定一个可行的迁移方案,平衡风险、成本和质量。

参考答案

  1. 前期准备

    • 统一思想,明确迁移目标(类型安全、可维护性、DX)。
    • 选定 TypeScript 版本和构建工具。
    • 建立编码规范(ESLint + @typescript-eslint)。
  2. 渐进式迁移策略

    • 允许 JS"allowJs": true
    • 混合编译:逐步将 .js 改为 .ts,未改的文件继续工作。
    • 逐模块迁移:从核心工具库、公共组件开始,再到业务模块。
    • 类型补充:先为 JS 模块写 .d.ts 声明文件,再逐步改写实现。
  3. 配置策略

    • 初始关闭 strict,开启 noImplicitAny: false
    • 随着迁移推进,逐步开启 noImplicitAnystrictNullChecks 等。
    • 最终目标 strict: true
  4. 自动化工具

    • 使用 ts-migratedts-gen 等工具批量生成初始类型。
    • 使用 any 占位标记待改进代码,配合 ESLint 规则逐步清理。
  5. 流程与质量保障

    • CI 中运行 tsc --noEmit,但初期可允许警告。
    • 建立 any 比例监控,逐步降低。
    • 代码审查时重点关注新增 TS 文件的质量。
  6. 团队培训

    • 开展 TypeScript 培训。
    • 建立常见模式的最佳实践文档。

评分维度

  • 渐进式策略清晰(30%)
  • tsconfig 分阶段配置合理(20%)
  • 提到自动化工具(15%)
  • 质量保障措施(20%)
  • 团队与流程考虑(15%)

常见错误

  • 一次性全量重写,风险极高
  • 一开始就开启 strict,导致大量错误无法推进
  • 迁移后不清理 any,类型质量差

延伸追问

  • 如何处理迁移过程中第三方库没有类型定义的问题?
  • 如何度量迁移进度和质量?

口头回答版

  • 统一思想,明确迁移目标(类型安全、可维护性、DX)。 - 选定 TypeScript 版本和构建工具。 - 建立编码规范(ESLint + @typescript-eslint)。 - 允许 JS:"allowJs": true。

FB-02-CP-R-001:如何在类型安全与开发效率(DX)之间取得平衡?

题型:综合开放题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:类型安全、DX、开发体验、平衡、工程实践 出现频率:中频 预计回答时长:10-15 分钟

题目描述: TypeScript 能提供更强的类型安全,但过度严格的类型系统可能导致开发效率下降、类型体操、编译变慢。请谈谈如何在类型安全与开发效率之间取得平衡。

参考答案

  1. 分场景设定严格程度

    • 核心领域模型、公共库、API 契约:严格类型。
    • 临时脚本、测试文件、原型代码:可适当放宽。
  2. 避免过度类型体操

    • 能用简单类型表达时不要用复杂条件类型。
    • 类型推导优于复杂标注。
    • 类型应服务于可读性和可维护性,而非炫技。
  3. 合理使用 any 和 unknown

    • 外部边界(解析 JSON、第三方库未声明类型)使用 unknown + 校验。
    • 遗留代码迁移期可用 any 占位,但必须标记 TODO。
    • 禁止在核心业务逻辑中长期存在 any。
  4. 工具链与流程优化

    • 使用 skipLibCheckincremental 等优化编译速度。
    • 类型检查与构建分离,开发时不阻塞热更新。
    • CI 中运行全量类型检查。
  5. 类型即文档

    • 为公共 API 写清晰类型,帮助开发者理解契约。
    • 使用 JSDoc/注释补充类型的业务语义。
  6. 团队约定

    • 制定 TypeScript 代码规范。
    • 定期进行类型质量复盘。
    • 鼓励使用 satisfiesas const 等现代特性提升类型精度而不增加负担。

评分维度

  • 分场景讨论严格程度(25%)
  • 提到避免类型体操(20%)
  • any/unknown 使用策略(20%)
  • 工具链与流程优化(20%)
  • 团队与规范(15%)

常见错误

  • 认为类型安全永远优先于一切
  • 认为 any 完全不能用
  • 为简单场景写过度复杂的类型

延伸追问

  • 你如何看待 "TypeScript 类型体操" 这种现象?
  • 在一个新项目中,你会如何制定 TypeScript 使用规范?

口头回答版

  • 核心领域模型、公共库、API 契约:严格类型。 - 临时脚本、测试文件、原型代码:可适当放宽。 - 能用简单类型表达时不要用复杂条件类型。 - 类型推导优于复杂标注。

FB-02-SD-R-004:TypeScript Compiler API 可以做什么?请设计一个应用场景。

题型:系统设计题 难度:⚫ 架构 岗位层级:架构师 面试知识域:02 TypeScript 标签:compiler api、typescript api、AST、代码生成、类型分析 出现频率:低频 预计回答时长:10-20 分钟

题目描述: 请介绍 TypeScript Compiler API 的能力,并设计一个实际应用场景(如自动生成类型、代码检查、自定义 transformer 等)。

参考答案

TypeScript Compiler API 允许直接调用 TypeScript 编译器的内部能力,主要包括:

  1. 解析与生成 AST

    • ts.createSourceFile() 解析源码为 AST。
    • 遍历节点进行代码分析或转换。
  2. 类型检查

    • ts.createProgram() 创建编译程序。
    • 获取类型信息、符号、诊断信息。
  3. 代码生成与转换

    • 自定义 Transformer 修改 AST。
    • 生成 .d.ts、代码文档、路由表等。
  4. 语言服务

    • 实现自定义 IDE 插件、自动补全、重构等。

应用场景:自动生成 API 类型

ts
import ts from 'typescript';

function generateTypes(sourceFile: ts.SourceFile) {
  ts.forEachChild(sourceFile, function visit(node) {
    if (ts.isInterfaceDeclaration(node)) {
      console.log('Interface:', node.name.text);
    }
    ts.forEachChild(node, visit);
  });
}

const sourceFile = ts.createSourceFile(
  'input.ts',
  'interface User { name: string }',
  ts.ScriptTarget.ESNext,
  true
);

generateTypes(sourceFile);

其他应用:

  • 基于路由文件生成菜单类型。
  • 自定义 ESLint 规则。
  • 自动提取 i18n key 并生成类型。
  • 编译期宏 / 代码注入。

评分维度

  • 说明 Compiler API 主要能力(30%)
  • 设计一个具体应用场景(30%)
  • 能写出关键 API 调用(20%)
  • 说明应用场景的业务价值(20%)

常见错误

  • 混淆 Compiler API 与 tsc 命令行
  • 不了解 AST 遍历的基本方式
  • 设计过于简单或不可行的场景

延伸追问

  • Compiler API 和 Babel AST 操作有什么区别?
  • 自定义 Transformer 在构建流程中如何集成?

参考资源

口头回答版

TypeScript Compiler API 允许直接调用 TypeScript 编译器的内部能力,主要包括: - ts.createSourceFile() 解析源码为 AST。 - 遍历节点进行代码分析或转换。 - ts.createProgram() 创建编译程序。


FB-02-CO-B-011:TypeScript 和 JavaScript 有什么关系?它的核心优势是什么?

题型:概念题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述: TypeScript 和 JavaScript 有什么关系?它的核心优势是什么。

参考答案

  • TypeScript 是 JavaScript 的超集,任何合法的 JS 都是合法的 TS,TS 最终会被编译为 JS。
  • 核心优势:
    • 静态类型检查,提前发现错误。
    • 更好的 IDE 支持(智能提示、跳转、重构)。
    • 增强代码可维护性,特别适合大型项目。
    • 提供接口、泛型、枚举、装饰器等高级语言特性。

评分维度

  • 说明超集与编译关系(37%)。
  • 至少列出 3 个优势(38%)。
  • 结合实际项目说明价值(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • TypeScript 是 JavaScript 的超集,任何合法的 JS 都是合法的 TS,TS 最终会被编译为 JS。 - 核心优势: - 静态类型检查,提前发现错误。 - 更好的 IDE 支持(智能提示、跳转、重构)。 - 增强代码可维护性,特别适合大型项目。

FB-02-CP-B-001:interfacetype 有什么区别?

题型:综合开放题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述interfacetype 有什么区别。

参考答案

  • interface 主要用于描述对象/类的形状,支持声明合并和 extends 继承。
  • type 可用于定义任意类型(联合、交叉、元组、映射、条件类型等),不支持声明合并,扩展用 &
  • 实际开发中:对象结构优先用 interface,联合/工具类型用 type

补充说明

在实际落地 interfacetype 有什么区别 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 说出声明合并差异(37%)。
  • 说出扩展方式差异(25%)。
  • 说出使用场景差异(25%)。
  • 提到底层等价性(class 与 interface 配合)(13%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • interface 主要用于描述对象/类的形状,支持声明合并和 extends 继承。 - type 可用于定义任意类型(联合、交叉、元组、映射、条件类型等),不支持声明合并,扩展用 &。 - 实际开发中:对象结构优先用 interface,联合/工具类型用 type。

FB-02-CP-B-002:anyunknownnever 的区别?

题型:综合开放题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述anyunknownnever 的区别。

参考答案

  • any:关闭类型检查,任何操作都允许,容易引入运行时错误,应尽量避免。
  • unknown:类型安全的 any,任何值都可赋给 unknown,但 unknown 不能赋给具体类型,也不能调用方法,必须先做类型收窄。
  • never:表示永远不会发生的类型,常用于抛出错误的函数、穷尽性检查。

补充说明

在实际落地 anyunknownnever 的区别 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 三者定义准确(50%)。
  • 能写出 unknown 收窄示例(25%)。
  • 能写出 never 穷尽检查示例(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • any:关闭类型检查,任何操作都允许,容易引入运行时错误,应尽量避免。 - unknown:类型安全的 any,任何值都可赋给 unknown,但 unknown 不能赋给具体类型,也不能调用方法,必须先做类型收窄。 - never:表示永远不会发生的类型,常用于抛出错误的函数、穷尽性检查。

FB-02-CD-B-003:什么是泛型?写一个实际应用的例子。

题型:手写代码题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 什么是泛型?写一个实际应用的例子。。

参考答案

泛型是在定义函数、接口、类时不预先指定具体类型,而在使用时再指定,从而提高代码复用性。

ts
function identity<T>(arg: T): T { return arg; }

interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

const userRes: ApiResponse<{ name: string }> = {
  code: 0,
  data: { name: "Alice" },
  message: "ok"
};

评分维度

  • 概念解释清楚(37%)。
  • 举例说明泛型函数或接口(38%)。
  • 提到类型推断(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

泛型是在定义函数、接口、类时不预先指定具体类型,而在使用时再指定,从而提高代码复用性。


FB-02-CO-B-012:TypeScript 中的类型推断是什么?什么情况下需要显式注解?

题型:概念题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述: TypeScript 中的类型推断是什么?什么情况下需要显式注解。

参考答案

  • 类型推断是 TypeScript 根据上下文自动推导类型的能力。
  • 常见推断场景:变量初始化、函数返回值、数组元素、泛型参数。
  • 需要显式注解的场景:
    • 函数参数没有足够上下文时。
    • 需要更精确类型(如字面量类型)时。
    • 复杂对象或 API 返回值。
    • 需要对外暴露的公共类型。

评分维度

  • 解释类型推断(37%)。
  • 举例说明自动推断(25%)。
  • 说明显式注解场景(38%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • 类型推断是 TypeScript 根据上下文自动推导类型的能力。 - 常见推断场景:变量初始化、函数返回值、数组元素、泛型参数。 - 需要显式注解的场景: - 函数参数没有足够上下文时。 - 需要更精确类型(如字面量类型)时。

FB-02-CP-B-003:readonlyconst 有什么区别?

题型:综合开放题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述readonlyconst 有什么区别。

参考答案

  • const 用于变量声明,表示变量标识符不能重新赋值。
  • readonly 用于属性,表示对象的该属性不能被重新赋值。
  • const 是运行时/语法层面的约束;readonly 是类型层面的约束,编译为 JS 后消失。
  • readonly 只对属性引用生效,如果属性是对象,对象内部仍可修改。

补充说明

在实际落地 readonlyconst 有什么区别 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 作用对象不同(25%)。
  • 约束层面不同(25%)。
  • 提到引用类型内部可变(25%)。
  • 能举例说明(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • const 用于变量声明,表示变量标识符不能重新赋值。 - readonly 用于属性,表示对象的该属性不能被重新赋值。 - const 是运行时/语法层面的约束;readonly 是类型层面的约束,编译为 JS 后消失。 - readonly 只对属性引用生效,如果属性是对象,对象内部仍可修改。

FB-02-CP-B-004:如何实现可选属性??undefined 有什么区别?

题型:综合开放题 难度:🟢 基础 岗位层级:初级 / 高级 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:3-5 分钟

题目描述: 如何实现可选属性??undefined 有什么区别。

参考答案

  • 可选属性用 ? 标记:age?: number 表示属性可以不存在,类型为 number | undefined(开启 strictNullChecks)。
  • age: undefined 表示属性必须存在,但值为 undefined。
  • 可选属性可以用 ? 访问,TypeScript 会做 undefined 检查。

补充说明

在实际落地 实现可选属性`` 和 undefined 有什么区别 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 区分可选和显式 undefined(50%)。
  • 说明 strictNullChecks 影响(25%)。
  • 举例说明(25%)。

二、进阶题

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • 可选属性用 ? 标记:age?: number 表示属性可以不存在,类型为 number | undefined(开启 strictNullChecks)。 - age: undefined 表示属性必须存在,但值为 undefined。 - 可选属性可以用 ? 访问,TypeScript 会做 undefined 检查。

FB-02-CD-A-004:什么是泛型约束?写一个带约束的泛型函数。

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 什么是泛型约束?写一个带约束的泛型函数。。

参考答案

泛型约束是对泛型参数的限定,确保泛型满足某些条件。

ts
interface HasLength {
  length: number;
}
function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}
logLength("hello");
logLength([1, 2, 3]);
// logLength(123); // 报错

也可以用 keyof 约束键名:

ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

评分维度

  • 解释约束目的(25%)。
  • 写出带约束的泛型函数(37%)。
  • 写出 keyof 约束示例(38%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

泛型约束是对泛型参数的限定,确保泛型满足某些条件。 (代码示例) 也可以用 keyof 约束键名: (代码示例)


FB-02-CO-A-011:解释 keyoftypeofinas 在类型系统中的作用。

题型:概念题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 解释 keyoftypeofinas 在类型系统中的作用。。

参考答案

  • keyof T:获取对象类型 T 的所有键的联合类型。
  • typeof x:获取变量 x 的类型。
  • in:在映射类型中遍历联合类型的每个成员。
  • as:类型断言或映射类型中的键重映射。
ts
interface User { name: string; age: number; }
type UserKeys = keyof User; // "name" | "age"

const user = { name: "Alice" };
type UserType = typeof user; // { name: string }

type ReadonlyUser = {
  [K in keyof User]: User[K];
};

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

评分维度

  • 四个关键字解释正确(50%)。
  • 能给出代码示例(50%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • keyof T:获取对象类型 T 的所有键的联合类型。 - typeof x:获取变量 x 的类型。 - in:在映射类型中遍历联合类型的每个成员。 - as:类型断言或映射类型中的键重映射。

FB-02-CD-A-005:什么是映射类型?手写 Partial<T>Readonly<T>Pick<T, K>

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 什么是映射类型?手写 Partial<T>Readonly<T>Pick<T, K>。。

参考答案

映射类型是基于已有类型生成新类型的工具。

ts
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

补充说明

在实际落地 映射类型手写 Partial<T>Readonly<T>Pick<T, K>。 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 解释映射类型(25%)。
  • 三个工具类型手写正确(75%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

映射类型是基于已有类型生成新类型的工具。 (代码示例)


FB-02-CD-A-006:解释条件类型,并手写 IsArray<T>Extract<T, U>

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 解释条件类型,并手写 IsArray<T>Extract<T, U>。。

参考答案

条件类型形如 T extends U ? X : Y,根据类型是否满足条件选择分支。

ts
type IsArray<T> = T extends any[] ? true : false;

type MyExtract<T, U> = T extends U ? T : never;
// 示例
type R = MyExtract<"a" | "b" | "c", "a" | "c">; // "a" | "c"

补充说明

在实际落地 解释条件类型,并手写 IsArray<T>Extract<T, U>。 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • 条件类型语法解释(37%)。
  • 手写 IsArray(25%)。
  • 手写 Extract 并解释 distributive conditional type(38%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

条件类型形如 T extends U ? X : Y,根据类型是否满足条件选择分支。


FB-02-CD-A-007:什么是 infer?用它实现 ReturnType<T>Parameters<T>

题型:手写代码题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 什么是 infer?用它实现 ReturnType<T>Parameters<T>。。

参考答案

infer 用于在条件类型中声明一个待推断的类型变量。

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

type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

评分维度

  • 解释 infer 作用(37%)。
  • ReturnType 实现正确(38%)。
  • Parameters 实现正确(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

infer 用于在条件类型中声明一个待推断的类型变量。 (代码示例)


FB-02-CO-A-012:解释 discriminated union(可辨识联合),并说明其优势。

题型:概念题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 解释 discriminated union(可辨识联合),并说明其优势。。

参考答案

可辨识联合是指多个对象类型有一个共同的“可辨识字段”(tag),通过该字段可以缩小类型范围。

ts
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rectangle"; width: number; height: number };

function area(s: Shape) {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "square": return s.side ** 2;
    case "rectangle": return s.width * s.height;
    default: const _exhaustive: never = s;
  }
}

优势:

  • 类型安全:编译器能自动收窄类型。
  • 可维护性:新增类型时,穷尽检查会提醒补全分支。

评分维度

  • 解释可辨识联合概念(37%)。
  • 写出示例(38%)。
  • 提到 never 穷尽检查(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

可辨识联合是指多个对象类型有一个共同的“可辨识字段”(tag),通过该字段可以缩小类型范围。 (代码示例) 优势: - 类型安全:编译器能自动收窄类型。 - 可维护性:新增类型时,穷尽检查会提醒补全分支。


FB-02-CO-A-013:如何处理第三方库没有类型声明的问题?

题型:概念题 难度:🟡 进阶 岗位层级:高级 / 资深 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:5-8 分钟

题目描述: 如何处理第三方库没有类型声明的问题。

参考答案

  1. 先安装 @types/xxx 声明包。
  2. 如果没有,可以手写 .d.ts 声明文件。
  3. 使用 declare module 补充类型。
  4. 临时用 // @ts-ignoreany,但不推荐滥用。
ts
// types/my-lib.d.ts
declare module "my-lib" {
  export function foo(): string;
  export const version: string;
}

评分维度

  • 提到 @types(28%)。
  • 会写 .d.ts 声明文件(43%)。
  • 提到 ts-ignore 的临时性和风险(29%)。

三、高级题

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  1. 先安装 @types/xxx 声明包。 2. 如果没有,可以手写 .d.ts 声明文件。 3. 使用 declare module 补充类型。 4. 临时用 // @ts-ignore 或 any,但不推荐滥用。

FB-02-CO-P-011:解释 TypeScript 的协变、逆变、双向协变和不变,并举例子。

题型:概念题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: 解释 TypeScript 的协变、逆变、双向协变和不变,并举例子。。

参考答案

  • 协变(Covariance):子类型可以赋值给父类型位置。Cat[]Animal[] 的子类型。
  • 逆变(Contravariance):父类型可以赋值给子类型位置。函数参数是逆变的:(animal: Animal) => void 可以赋值给 (cat: Cat) => void
  • 不变(Invariance):只能完全相同类型赋值。如泛型参数默认。
  • 双向协变:TypeScript 早期允许函数参数双向协变,--strictFunctionTypes 关闭时会出现,可能导致类型不安全。

评分维度

  • 四个概念解释清楚(50%)。
  • 能举例说明协变/逆变(37%)。
  • 提到 strictFunctionTypes(13%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • 协变(Covariance):子类型可以赋值给父类型位置。 Cat[] 是 Animal[] 的子类型。 - 逆变(Contravariance):父类型可以赋值给子类型位置。 函数参数是逆变的:(animal: Animal) => void 可以赋值给 (cat: Cat) => void。

FB-02-CO-P-012:什么是类型体操?请实现 TupleToUnion<T>DeepReadonly<T>

题型:概念题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: 什么是类型体操?请实现 TupleToUnion<T>DeepReadonly<T>。。

参考答案

类型体操指利用 TS 类型系统解决复杂类型推导问题。

ts
type TupleToUnion<T extends any[]> = T[number];

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};

评分维度

  • 解释类型体操(20%)。
  • TupleToUnion 实现正确(30%)。
  • DeepReadonly 实现合理(30%)。
  • 提到可读性权衡(20%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

类型体操指利用 TS 类型系统解决复杂类型推导问题。 (代码示例)


FB-02-SD-P-001:如何设计一个类型安全的前端 API 请求层?

题型:系统设计题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: 如何设计一个类型安全的前端 API 请求层。

参考答案

ts
interface ApiResponse<T> {
  code: number;
  data: T;
  message?: string;
}

async function request<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const res = await fetch(url, options);
  const json: ApiResponse<T> = await res.json();
  if (json.code !== 0) throw new Error(json.message);
  return json.data;
}

// 使用
interface User { id: number; name: string; }
const user = await request<User>("/api/user/1");

设计要点:

  • 泛型保留返回数据类型。
  • 统一错误处理。
  • 可扩展请求/响应拦截器。
  • 使用 Zod/Yup 做运行时校验,弥补 TS 只在编译期有效的不足。

评分维度

  • 泛型请求函数设计(30%)。
  • 统一响应结构(20%)。
  • 错误处理机制(20%)。
  • 提到运行时校验(30%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

(代码示例) 设计要点: - 泛型保留返回数据类型。 - 可扩展请求/响应拦截器。 - 使用 Zod/Yup 做运行时校验,弥补 TS 只在编译期有效的不足。


FB-02-SC-P-001:解释 declarenamespacemodule 的区别与使用场景。

题型:场景设计题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: 解释 declarenamespacemodule 的区别与使用场景。。

参考答案

  • declare:告诉 TypeScript 某个变量/模块/类型在运行时已存在,不做实现。
  • namespace:用于组织代码,内部可包含类型、值、函数等。现代模块化开发中使用减少。
  • module
    • .d.tsdeclare module "xxx" 为无类型库补充类型。
    • 早期 TS 中 modulenamespace 类似,现在推荐使用 ES Module。

补充说明

在实际落地 解释 declarenamespacemodule 的区别与使用场景。 时,建议结合 类型安全、泛型、类型推断 的真实场景做验证。重点关注可观测性埋点、异常降级路径和性能基线回归;同时通过灰度发布、指标看板和复盘机制持续迭代,确保方案从“能跑”演进为“可维护、可扩展”。 评分维度

  • declare 作用(25%)。
  • namespace 作用(25%)。
  • module 声明文件用法(37%)。
  • 现代项目使用建议(13%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

  • declare:告诉 TypeScript 某个变量/模块/类型在运行时已存在,不做实现。 - namespace:用于组织代码,内部可包含类型、值、函数等。 现代模块化开发中使用减少。 - module: - 在 .d.ts 中 declare module "xxx" 为无类型库补充类型。

FB-02-CO-P-013:Vue3 和 React 中是如何利用 TypeScript 类型系统的?

题型:概念题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: Vue3 和 React 中是如何利用 TypeScript 类型系统的。

参考答案

Vue3:

  • 使用 defineProps 的泛型或运行时声明推导 props 类型。
  • refreactivecomputed 都有完整泛型支持。
  • 组合式函数通过泛型实现类型安全的复用。

React:

  • 函数组件用 React.FC 或直接注解 props 类型。
  • useStateuseRefuseReducer 都支持泛型。
  • 事件处理类型:React.MouseEventReact.ChangeEvent 等。

共同点:都利用泛型保留状态类型,避免隐式 any。

评分维度

  • Vue3 类型使用(37%)。
  • React 类型使用(38%)。
  • 提到泛型保留类型(25%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

Vue3: - 使用 defineProps 的泛型或运行时声明推导 props 类型。 - ref、reactive、computed 都有完整泛型支持。 - 组合式函数通过泛型实现类型安全的复用。 React: - 函数组件用 React.FC 或直接注解 props 类型。


FB-02-SC-P-002:TypeScript 编译配置 strict: true 会开启哪些检查?实际项目中如何取舍?

题型:场景设计题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: TypeScript 编译配置 strict: true 会开启哪些检查?实际项目中如何取舍。

参考答案

strict: true 开启:

  • noImplicitAny
  • strictNullChecks
  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization
  • noImplicitThis
  • alwaysStrict

取舍建议:

  • 新项目强烈推荐直接开启 strict: true
  • 老项目迁移可逐步开启,先用 noImplicitAny: false 过渡。
  • 不要因噎废食全部关闭,核心项目应保证 strictNullChecksnoImplicitAny 开启。

评分维度

  • 列出主要 strict 子项(40%)。
  • 新项目建议(20%)。
  • 老项目迁移策略(20%)。
  • 提到具体关键项(20%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

strict: true 开启: - noImplicitAny - strictNullChecks - strictFunctionTypes - strictBindCallApply - strictPropertyInitialization - noImplicitThis - alwaysStrict 取舍建议: - 新项目强烈推荐直接开启 strict: true。 - 老项目迁移可逐步开启,先用 noImplicitAny: false 过渡。 - 不要因噎废食全部关闭,核心项目应保证 strictNullChecks 和 noImplicitAny 开启。


FB-02-CP-P-001:satisfies 与类型注解 :T、类型断言 as T 有什么区别?

题型:综合开放题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述satisfies 与类型注解 :T、类型断言 as T 有什么区别。

参考答案

方式约束结构保留具体推断类型安全性
类型注解 :T❌ 会被放宽到 T安全
as T❌ 可能断言错误类型不安全
satisfies T安全

示例:

ts
type Config = { host: string; port: number };

const c1: Config = { host: "localhost", port: 3000 };
// c1.host 类型为 string

const c2 = { host: "localhost", port: 3000 } as Config;
// c2.host 类型为 string,且可能绕过额外属性检查

const c3 = { host: "localhost", port: 3000 } satisfies Config;
// c3.host 类型为 "localhost"

satisfies 适合需要既约束结构又保留字面量类型的场景,如配置对象、palette、路由表。

评分维度

  • 能区分三者核心差异(40%)。
  • 能举例说明具体类型保留(30%)。
  • 能说明 as 的安全风险(30%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

| 方式 | 约束结构 | 保留具体推断类型 | 安全性 | |------|---------|-----------------|--------| | 类型注解 :T | ✅ | ❌ 会被放宽到 T | 安全 | | as T | ❌ 可能断言错误类型 | ✅ | 不安全 | | satisfies T | ✅ | ✅ | 安全 | 示例: (代码示例) satisfies 适合需要既约束结构又保留字面量类型的场景,如配置对象、palette、路由表。


FB-02-CO-P-014:什么是 Branded Type?它解决了什么问题?

题型:概念题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: 什么是 Branded Type?它解决了什么问题。

参考答案

Branded Type 通过交叉类型给原始类型加一个不可见的编译期标签,使其在类型系统中可区分。

ts
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };

function queryOrder(id: OrderId) {}
const uid = "u123" as UserId;
queryOrder(uid); // ❌ 编译报错

解决的问题:

  • 防止把语义不同的同类型值混用(如用户 ID 和订单 ID 都是 string)。
  • 零运行时开销,因为标签只在类型系统中存在。

适用场景:各种 ID、Email、URL、金额、日期字符串等。

评分维度

  • 能解释实现原理(30%)。
  • 能说明解决的问题(35%)。
  • 能举例并提到零运行时开销(35%)。

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

Branded Type 通过交叉类型给原始类型加一个不可见的编译期标签,使其在类型系统中可区分。 (代码示例) 解决的问题: - 防止把语义不同的同类型值混用(如用户 ID 和订单 ID 都是 string)。 - 零运行时开销,因为标签只在类型系统中存在。 适用场景:各种 ID、Email、URL、金额、日期字符串等。


FB-02-PE-P-001:如何优化大型 TypeScript 项目的编译性能?

题型:性能优化题 难度:🟣 深入 岗位层级:资深 / 架构 面试知识域:02 TypeScript 标签:类型安全、泛型、类型推断、类型收窄、tsconfig 出现频率:中频 预计回答时长:7-10 分钟

题目描述: 如何优化大型 TypeScript 项目的编译性能。

参考答案

配置层面:

  • 开启 skipLibCheck: true 跳过 .d.ts 类型检查。
  • 使用 isolatedModules: true 配合 swc/esbuild 转译。
  • 合理设置 include / exclude,避免扫描无关文件。
  • 使用 moduleResolution: bundler

工程层面:

  • Project References:把大项目拆分为多个子项目,独立编译。
  • 类型检查与转译分离:swc/esbuild 做转译,tsc --noEmit 做类型检查。
  • fork-ts-checker-webpack-plugin 在 Webpack 中并行类型检查。

代码层面:

  • 避免深层嵌套的条件类型和无限递归类型。
  • 控制联合类型大小,使用可辨识联合替代巨型联合。
  • 为复杂函数显式标注返回类型,减少推断负担。

评分维度

  • 能说出 3 个以上配置优化(30%)。
  • 能说明 Project References 或类型检查分离(30%)。
  • 能给出代码层面建议(40%)。

面试准备建议

  1. 手写工具类型:Partial、Readonly、Pick、Omit、ReturnType、Parameters、Exclude、Extract。
  2. 理解泛型约束、条件类型、infer、映射类型。
  3. 准备项目案例:说明 TS 如何帮助你发现 bug 或安全重构。
  4. 类型体操量力而行,面试中如果问到,先写简单版本,再逐步优化。

领域编号:F02 TypeScript
最后更新:2026-06-24

常见错误

  • 回答停留在定义复述,缺少真实项目中的取舍与折中。
  • 只讲正常路径,不提超时、降级、兼容等边界情况。
  • 对关键指标和取舍缺乏量化意识。

口头回答版

配置层面: - 开启 skipLibCheck: true 跳过 .d.ts 类型检查。 - 使用 isolatedModules: true 配合 swc/esbuild 转译。 - 合理设置 include / exclude,避免扫描无关文件。 - 使用 moduleResolution: bundler。

基于 MIT 协议发布