Skip to content

TypeScript 2026 面试题汇总

一句话定位:聚焦 TypeScript 5.x 类型系统、工程实践与高级类型体操,不收录已废弃的 namespace/enum 滥用等过时模式

目录


基础类型与类型系统

1. TypeScript 的类型系统是结构化的还是名义化的?有什么区别?

答案要点

  • TypeScript 采用结构化类型系统(Structural Typing),也叫"鸭子类型"
  • 只要两个类型的结构兼容(属性和方法一致),就认为它们是兼容的,不关心类型名称
  • 与 Java/C# 的名义化类型系统(Nominal Typing)不同,后者必须显式声明实现关系
typescript
interface Point {
  x: number;
  y: number;
}

interface Coordinate {
  x: number;
  y: number;
}

const p: Point = { x: 1, y: 2 };
const c: Coordinate = p; // ✅ 结构兼容,无需显式声明关系

追问:如何在 TypeScript 中模拟名义类型?(branded types / unique symbol)


2. anyunknownnever 三者有什么区别?分别在什么场景使用?

答案要点

  • any:关闭类型检查,可以赋值给任何类型,也可以接受任何类型。尽量避免使用
  • unknown:类型安全的顶层类型,可以接受任何类型的值,但使用前必须进行类型收窄
  • never:底层类型,表示永远不会出现的值(如抛异常的函数返回值、exhaustive check)
typescript
// unknown 必须收窄后才能使用
function process(val: unknown) {
  if (typeof val === 'string') {
    console.log(val.toUpperCase()); // ✅ 收窄后可用
  }
}

// never 用于穷尽检查
type Shape = 'circle' | 'square';
function getArea(shape: Shape) {
  switch (shape) {
    case 'circle': return Math.PI;
    case 'square': return 1;
    default:
      const _exhaustive: never = shape; // 编译期保证所有分支已覆盖
      return _exhaustive;
  }
}

追问{} 类型和 object 类型分别表示什么?


3. typeinterface 有什么区别?什么时候用哪个?

答案要点

  • interface:可以声明合并(declaration merging)、用 extends 继承、仅描述对象结构
  • type:不可声明合并、用 & 交叉组合、可表示联合类型/元组/映射类型等所有类型
  • 实践建议:对外暴露的 API 用 interface(方便扩展),内部复杂类型用 type
typescript
// interface 声明合并
interface Window {
  myCustomProp: string;
}

// type 可以表示联合类型
type Result = Success | Failure;

// type 可以表示映射类型
type Readonly<T> = { readonly [K in keyof T]: T[K] };

追问interface 的声明合并在什么场景下有用?(扩展第三方库类型)


4. 元组(Tuple)类型和数组类型有什么区别?

答案要点

  • 数组类型string[]Array<string>,元素类型一致,长度不固定
  • 元组类型[string, number],每个位置有确定类型,长度固定
  • TS 4.0+ 支持可变长元组(Variadic Tuple Types)命名元组元素
typescript
// 基础元组
const pair: [string, number] = ['age', 25];

// 命名元组(提升可读性)
type Range = [start: number, end: number];

// 可变长元组
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
type R = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

// const 断言推导为元组
const colors = ['red', 'green', 'blue'] as const;
// 类型为 readonly ["red", "green", "blue"]

追问as const 断言的作用是什么?与 readonly 有什么关系?


5. 枚举(enum)的底层实现是什么?2026 年还推荐使用吗?

答案要点

  • 数字枚举编译为双向映射对象(值→名称,名称→值),字符串枚举只有单向映射
  • const enum 在编译时内联替换,不生成运行时代码
  • 2026 年推荐:优先使用 as const 对象或联合类型替代 enum,因为:
    • enum 是 TypeScript 少数生成运行时代码的特性,与"类型擦除"理念不一致
    • TC39 正在推进的 --erasableSyntaxOnly 模式不支持非 const enum
typescript
// ❌ 不推荐:传统 enum
enum Direction {
  Up,    // 0
  Down,  // 1
}

// ✅ 推荐:as const + 联合类型
const Direction = {
  Up: 'UP',
  Down: 'DOWN',
} as const;

type Direction = (typeof Direction)[keyof typeof Direction];
// "UP" | "DOWN"

追问const enum 有什么陷阱?(isolatedModules 下不可用、跨文件问题)


6. TypeScript 中的字面量类型和模板字面量类型是什么?

答案要点

  • 字面量类型:将值本身作为类型,如 'hello'42true
  • 模板字面量类型(Template Literal Types):TS 4.1+ 引入,用模板字符串语法组合字面量类型
  • 常用于构建强类型事件名、CSS 属性值、路由路径等
typescript
// 模板字面量类型
type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
// "onClick" | "onFocus" | "onBlur"

// 组合生成大量类型
type CSSMargin = `margin-${'top' | 'right' | 'bottom' | 'left'}`;
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"

// 配合 infer 做模式匹配
type ExtractParam<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParam<Rest>
    : S extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParam<'/user/:id/post/:postId'>;
// "id" | "postId"

追问:模板字面量类型有什么性能限制?(联合类型数量爆炸)


接口与类型别名

7. 交叉类型(Intersection)和联合类型(Union)的区别是什么?

答案要点

  • 联合类型 A | B:值可以是 A 或 B,只能访问共有成员
  • 交叉类型 A & B:值同时满足 A 和 B,可以访问所有成员
  • 对于原始类型交叉,结果可能是 never(如 string & number
typescript
// 联合类型:只能访问共有属性
type Cat = { name: string; meow(): void };
type Dog = { name: string; bark(): void };

function getName(pet: Cat | Dog) {
  return pet.name; // ✅ 共有属性
  // pet.meow();   // ❌ 不确定是 Cat
}

// 交叉类型:合并所有属性
type CatDog = Cat & Dog;
const hybrid: CatDog = {
  name: 'Hybrid',
  meow() {},
  bark() {},
};

追问:交叉类型遇到属性冲突时会发生什么?


8. 如何实现一个类型,让对象的某些属性变为可选?

答案要点

  • 使用 Partial 让所有属性可选,但通常只需部分可选
  • 组合 Omit + Partial + Pick 实现精确控制
typescript
// 让指定属性变为可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
  avatar: string;
}

// 只有 email 和 avatar 可选
type CreateUserInput = PartialBy<User, 'email' | 'avatar'>;
// { id: number; name: string; email?: string; avatar?: string }

追问RequiredPartial 的实现原理?(映射类型 + -? 修饰符)


9. 索引签名和 Record 工具类型的区别?

答案要点

  • 索引签名{ [key: string]: T },键类型只能是 stringnumbersymbol
  • Record<K, V>{ [P in K]: V },键可以是联合类型字面量,更精确
  • TS 4.4+ 支持 Symbol 和模板字面量作为索引签名
typescript
// 索引签名:键范围宽泛
interface StringMap {
  [key: string]: number;
}

// Record:键是精确的联合类型
type Fruit = 'apple' | 'banana' | 'cherry';
const prices: Record<Fruit, number> = {
  apple: 3,
  banana: 2,
  cherry: 5,
};
// 漏掉任何一个 key 都会报错 ✅

追问:索引签名和明确属性共存时有什么约束?


泛型

10. 什么是泛型?为什么需要泛型?

答案要点

  • 泛型是参数化类型,允许在定义函数/类/接口时不预先指定具体类型,使用时再确定
  • 核心价值:在保持类型安全的同时实现代码复用
  • 没有泛型,要么丢失类型信息(用 any),要么为每种类型写重复代码
typescript
// 没有泛型:丢失类型信息
function firstAny(arr: any[]): any {
  return arr[0];
}

// 有泛型:保持类型安全
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = first([1, 2, 3]);    // number | undefined
const str = first(['a', 'b']);   // string | undefined

追问:泛型参数的默认值怎么用?(<T = string>


11. 泛型约束(Generic Constraints)怎么用?extends 在泛型中的含义?

答案要点

  • T extends U 表示 T 必须是 U 的子类型,用于限制泛型的范围
  • 常见约束模式:extends objectextends stringextends { length: number }
  • keyof + extends 组合可以约束属性名
typescript
// 约束 T 必须有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest('hello', 'hi');     // ✅ string 有 length
longest([1, 2], [1, 2, 3]); // ✅ 数组有 length
// longest(10, 20);          // ❌ number 没有 length

// keyof 约束属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 25 };
getProperty(user, 'name'); // string
// getProperty(user, 'foo'); // ❌ 'foo' 不是 keyof User

追问extends 在条件类型中是什么含义?(是"可赋值给"而非"继承")


12. 什么是泛型推断(infer)?给出典型使用场景

答案要点

  • infer 只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量
  • 常用于提取函数返回值、Promise 内部类型、数组元素类型等
typescript
// 提取函数返回类型(ReturnType 的实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type R3 = UnwrapPromise<Promise<Promise<number>>>; // number

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

// 提取函数第一个参数类型
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type R5 = FirstArg<(name: string, age: number) => void>; // string

追问infer 在同一位置多次推断同一变量时会怎样?(协变位置取联合,逆变位置取交叉)


13. 泛型在 React 组件中怎么使用?

答案要点

  • 泛型组件让 props 类型可以参数化,提升组件复用性
  • 常见场景:通用列表组件、表单组件、Select 组件
tsx
// 泛型列表组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 使用时自动推断 T
<List
  items={[{ id: 1, name: 'Alice' }]}
  renderItem={user => <span>{user.name}</span>}
  keyExtractor={user => String(user.id)}
/>

追问forwardRef 如何与泛型组件配合?(TS 5.x 中直接支持)


类型守卫与类型收窄

14. TypeScript 有哪些类型收窄(Type Narrowing)的方式?

答案要点

  • typeof:收窄原始类型(string/number/boolean/symbol/bigint/undefined/function
  • instanceof:收窄类实例
  • in:检查属性是否存在
  • 等值检查=== null=== undefined
  • 可辨识联合(Discriminated Union):通过共有字面量属性区分
  • 自定义类型谓词is 关键字
  • satisfies:TS 4.9+,验证类型但不改变推断
typescript
// 可辨识联合 —— 最推荐的模式
type Success = { status: 'ok'; data: string };
type Failure = { status: 'error'; message: string };
type Result = Success | Failure;

function handle(result: Result) {
  if (result.status === 'ok') {
    console.log(result.data);    // ✅ 收窄为 Success
  } else {
    console.log(result.message); // ✅ 收窄为 Failure
  }
}

追问typeof null 返回什么?TypeScript 如何处理这个 JS 历史遗留问题?


15. 自定义类型守卫(Type Predicate)怎么写?什么时候需要?

答案要点

  • 返回类型标注为 paramName is Type,告诉编译器函数返回 true 时参数是指定类型
  • 当 TypeScript 无法自动收窄时使用,如跨函数调用、复杂对象判断
typescript
interface Fish { swim(): void }
interface Bird { fly(): void }

// 自定义类型守卫
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // ✅ 收窄为 Fish
  } else {
    pet.fly();  // ✅ 收窄为 Bird
  }
}

// 配合 filter 使用(过滤 null/undefined)
const values: (string | null)[] = ['a', null, 'b'];
const nonNull = values.filter((v): v is string => v !== null);
// nonNull: string[]

追问asserts 关键字和 is 有什么区别?(assertion functions)


16. satisfies 运算符的作用是什么?和类型标注有什么区别?

答案要点

  • satisfies 验证表达式符合某个类型,但保留原始推断类型,不拓宽
  • 类型标注 :Type 会将变量类型固定为声明的类型,丢失字面量信息
  • TS 4.9+ 引入,解决了"既要类型检查,又要保留推断"的矛盾
typescript
type Colors = Record<string, [number, number, number] | string>;

// 用类型标注:丢失具体信息
const colorsA: Colors = {
  red: [255, 0, 0],
  green: '#00ff00',
};
// colorsA.red 的类型是 [number, number, number] | string  ❌ 不精确

// 用 satisfies:保留推断
const colorsB = {
  red: [255, 0, 0],
  green: '#00ff00',
} satisfies Colors;
// colorsB.red 的类型是 [number, number, number]  ✅ 精确
// colorsB.green 的类型是 string  ✅ 精确

追问satisfies + as const 组合使用的场景?


高级类型

17. 条件类型(Conditional Types)怎么理解?什么是分布式条件类型?

答案要点

  • 条件类型T extends U ? X : Y,根据类型关系选择不同分支
  • 分布式条件类型:当 T 是联合类型且为裸类型参数时,会自动分发到每个成员
  • 可以用 [T] extends [U] 包裹来阻止分发
typescript
// 基础条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<number>;  // false

// 分布式条件类型
type Exclude<T, U> = T extends U ? never : T;
type C = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
// 等价于:('a' extends 'a' ? never : 'a') | ('b' extends 'a' ? never : 'b') | ('c' extends 'a' ? never : 'c')

// 阻止分发
type IsNever<T> = [T] extends [never] ? true : false;
type D = IsNever<never>; // true(如果不包裹,never 分发后什么都不剩,结果是 never)

追问never 在条件类型中的分发行为是什么?


18. 映射类型(Mapped Types)是什么?如何使用修饰符?

答案要点

  • 映射类型:通过 in keyof 遍历已有类型的键来生成新类型
  • 修饰符readonly?,可以用 +/- 前缀来添加或移除
  • 键重映射(Key Remapping):TS 4.1+ 通过 as 子句改变键名
typescript
// 基础映射类型
type Optional<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };     // 移除可选
type ReadonlyAll<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 移除只读

// 键重映射:Getter 类型
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// 过滤键
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K]
};
type WithoutStrings = OmitByType<Person, string>;
// { age: number }

追问:映射类型中 keyof Tin 的关系是什么?


19. 协变和逆变是什么?在 TypeScript 中怎么体现?

答案要点

  • 协变(Covariance):子类型关系保持方向。Dog extends AnimalArray<Dog> extends Array<Animal>
  • 逆变(Contravariance):子类型关系反转。出现在函数参数位置
  • 双变(Bivariance):TypeScript 默认函数参数是双变的,开启 strictFunctionTypes 后变为逆变
  • 不变(Invariance):既不协变也不逆变
typescript
class Animal { name = '' }
class Dog extends Animal { bark() {} }

// 协变:返回值位置
type Producer<T> = () => T;
let produceDog: Producer<Dog> = () => new Dog();
let produceAnimal: Producer<Animal> = produceDog; // ✅ 协变

// 逆变:参数位置(strictFunctionTypes: true)
type Consumer<T> = (arg: T) => void;
let consumeAnimal: Consumer<Animal> = (a: Animal) => {};
let consumeDog: Consumer<Dog> = consumeAnimal; // ✅ 逆变
// consumeAnimal = consumeDog; // ❌ 反过来不行

// 利用逆变实现 UnionToIntersection
type UnionToIntersection<U> =
  (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
    ? I
    : never;

type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }

追问:为什么 strictFunctionTypes 只对函数表达式生效,对方法声明不生效?


20. 什么是 Branded Types(品牌类型)?怎么实现?

答案要点

  • TypeScript 的结构类型系统下,结构相同的类型可以互相赋值
  • Branded Types 通过添加一个虚拟的品牌属性,实现运行时无开销的名义类型
  • 常用于:用户 ID vs 订单 ID、像素 vs 百分比等需要区分的同基础类型场景
typescript
// 声明品牌类型
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

// 创建函数(运行时零开销)
function createUserId(id: string): UserId {
  return id as UserId;
}

function createOrderId(id: string): OrderId {
  return id as OrderId;
}

function getUser(id: UserId) { /* ... */ }

const userId = createUserId('user_123');
const orderId = createOrderId('order_456');

getUser(userId);  // ✅
// getUser(orderId); // ❌ OrderId 不能赋值给 UserId

追问:除了 __brand,还有其他实现品牌类型的方式吗?(unique symbol


工具类型

21. TypeScript 内置工具类型有哪些?实现原理?

答案要点

常用内置工具类型及其实现:

typescript
// 1. Partial:所有属性变可选
type Partial<T> = { [K in keyof T]?: T[K] };

// 2. Required:所有属性变必选
type Required<T> = { [K in keyof T]-?: T[K] };

// 3. Readonly:所有属性变只读
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// 4. Pick:选取指定属性
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// 5. Omit:排除指定属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 6. Record:构造键值对类型
type Record<K extends keyof any, T> = { [P in K]: T };

// 7. Exclude:从联合类型中排除
type Exclude<T, U> = T extends U ? never : T;

// 8. Extract:从联合类型中提取
type Extract<T, U> = T extends U ? T : never;

// 9. NonNullable:排除 null 和 undefined
type NonNullable<T> = T & {};

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

// 11. Parameters:提取函数参数类型
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

// 12. Awaited:递归解包 Promise(TS 4.5+)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

追问NonNullableExclude<T, null | undefined> 改为 T & {} 的原因?


22. OmitPick 有什么区别?如何实现深层 Partial?

答案要点

  • Pick<T, K> 选取属性,Omit<T, K> 排除属性,互为补集
  • 深层 Partial 需要递归映射
typescript
// 深层 Partial
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  db: {
    host: string;
    port: number;
    options: {
      ssl: boolean;
      timeout: number;
    };
  };
  log: {
    level: string;
  };
}

// 所有层级的属性都变为可选
type PartialConfig = DeepPartial<Config>;
// db?: { host?: string; port?: number; options?: { ssl?: boolean; timeout?: number } }

// 深层 Readonly
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

追问:递归类型有深度限制吗?(TS 有内置的递归深度限制,约 50 层)


23. 如何实现 StrictOmit,让 K 必须是 T 的已知键?

答案要点

  • 内置 Omit<T, K> 中 K 约束为 keyof any(即 string | number | symbol),不会报错即使 K 不在 T 中
  • StrictOmit 将 K 约束为 keyof T,传入不存在的键会编译报错
typescript
// 内置 Omit 的问题
interface User { name: string; age: number }
type T1 = Omit<User, 'foo'>; // 不报错,但 'foo' 不是 User 的键 ❌

// StrictOmit
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type T2 = StrictOmit<User, 'name'>; // ✅ { age: number }
// type T3 = StrictOmit<User, 'foo'>; // ❌ 编译错误

追问:在什么场景下需要 StrictOmit


类型体操

24. 如何实现 UnionToIntersection(联合转交叉)?

答案要点

  • 利用函数参数的逆变特性:将联合类型分发到函数参数位置,再用 infer 推断
  • 逆变位置的多个候选类型会被推断为交叉类型
typescript
type UnionToIntersection<U> =
  (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
    ? I
    : never;

// 步骤分解:
// 1. U = A | B
// 2. 分发:(arg: A) => void | (arg: B) => void
// 3. 推断:infer I 在逆变位置,A & B
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }

追问:能实现 IntersectionToUnion 吗?(不能,TS 无法反向拆解交叉类型)


25. 如何实现 TupleToUnion(元组转联合)?

答案要点

  • 使用索引访问 T[number] 即可将元组所有元素类型转为联合
typescript
type TupleToUnion<T extends readonly any[]> = T[number];

type T1 = TupleToUnion<[string, number, boolean]>;
// string | number | boolean

// 结合 as const 使用
const STATUS = ['active', 'inactive', 'pending'] as const;
type Status = TupleToUnion<typeof STATUS>;
// "active" | "inactive" | "pending"

追问:如何实现 UnionToTuple?(非常复杂,需要利用函数重载推断的顺序特性)


26. 如何实现一个 DeepRequired 类型?

答案要点

  • 递归遍历所有属性,用 -? 移除可选修饰符
  • 需要处理数组、函数等特殊情况
typescript
type DeepRequired<T> = T extends (...args: any[]) => any
  ? T  // 函数不递归
  : T extends object
    ? { [K in keyof T]-?: DeepRequired<T[K]> }
    : T;

interface FormData {
  user?: {
    name?: string;
    address?: {
      city?: string;
      zip?: string;
    };
  };
  tags?: string[];
}

type RequiredForm = DeepRequired<FormData>;
// { user: { name: string; address: { city: string; zip: string } }; tags: string[] }

追问:如何实现只递归 N 层的 DeepPartial


27. 如何实现路径类型(Path Types),提取对象的所有嵌套路径?

答案要点

  • 递归遍历对象键,用模板字面量类型拼接路径
  • 实际框架如 react-hook-form、lodash 的 get 方法都使用类似技术
typescript
type Path<T, Prefix extends string = ''> = T extends object
  ? {
      [K in keyof T & string]: K | `${K}.${Path<T[K]>}`
    }[keyof T & string]
  : never;

interface User {
  name: string;
  address: {
    city: string;
    zip: string;
  };
  tags: string[];
}

type UserPaths = Path<User>;
// "name" | "address" | "address.city" | "address.zip" | "tags"

// 根据路径获取值类型
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;

type CityType = PathValue<User, 'address.city'>; // string

追问:这种递归类型在属性很多时会有什么性能问题?


28. 实现一个 Trim<S> 类型,去除字符串类型两端的空格

答案要点

  • 利用模板字面量类型 + 递归,逐个字符匹配空格并去除
typescript
type Whitespace = ' ' | '\n' | '\t';

type TrimLeft<S extends string> =
  S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S;

type TrimRight<S extends string> =
  S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S;

type Trim<S extends string> = TrimLeft<TrimRight<S>>;

type R1 = Trim<'  hello  '>;    // "hello"
type R2 = Trim<'\n  world \t'>; // "world"

追问:如何实现 Replace<S, From, To>ReplaceAll


模块与声明文件

29. .d.ts 声明文件的作用是什么?declare 关键字什么时候用?

答案要点

  • .d.ts 文件:只包含类型声明,不包含实现代码,用于为 JS 库提供类型信息
  • declare:声明一个已在运行时存在但 TS 不知道的变量/函数/类/模块
  • 三种来源:内置(lib.d.ts)、@types/* 包、项目自定义
typescript
// 声明全局变量(如 CDN 引入的库)
declare const gtag: (...args: any[]) => void;

// 声明模块(为无类型的 npm 包补类型)
declare module 'untyped-lib' {
  export function doSomething(input: string): number;
}

// 声明全局类型扩展
declare global {
  interface Window {
    __APP_VERSION__: string;
  }
}

// 声明 CSS Modules
declare module '*.module.css' {
  const classes: { [key: string]: string };
  export default classes;
}

// 声明图片等资源
declare module '*.png' {
  const src: string;
  export default src;
}

追问declare moduledeclare namespace 的区别?


30. TypeScript 的模块解析策略有哪些?moduleResolution 怎么配?

答案要点

  • node(Node.js 经典策略):按 node_modules 查找,支持 index.ts
  • node16 / nodenext:支持 ESM + CJS 双模式,package.jsonexports 字段
  • bundler(TS 5.0+,推荐):适用于打包器环境,支持 exports 但不强制文件扩展名
json
// tsconfig.json 推荐配置(2026)
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "module": "esnext",
    "target": "esnext",
    "verbatimModuleSyntax": true
  }
}
  • verbatimModuleSyntax(TS 5.0+):取代 isolatedModules,要求 import type 显式标记类型导入
typescript
// ✅ 正确:类型导入必须用 import type
import type { User } from './types';
import { createUser } from './utils';

// ❌ 错误:不使用 import type 导入纯类型
import { User } from './types'; // verbatimModuleSyntax 报错

追问package.jsonexports 字段如何影响 TypeScript 类型解析?


31. 如何为第三方 JS 库编写类型声明?

答案要点

  • 优先查看 DefinitelyTyped(@types/*),没有再手写
  • 创建 *.d.ts 文件,使用 declare module 声明模块类型
typescript
// types/awesome-lib.d.ts
declare module 'awesome-lib' {
  interface Options {
    timeout?: number;
    retries?: number;
  }

  interface Result {
    data: unknown;
    status: number;
  }

  // 默认导出
  export default function awesomeLib(url: string, options?: Options): Promise<Result>;

  // 命名导出
  export function configure(options: Options): void;
  export const version: string;
}
  • tsconfig.json 中配置 typeRootspaths 确保能找到声明文件

追问@types/* 包的版本如何与库版本对应?


TypeScript 编译与配置

32. tsconfig.json 中最重要的配置项有哪些?2026 年推荐怎么配?

答案要点

  • strict: true:开启所有严格类型检查(必须)
  • target:编译目标,现代项目用 ES2022ESNext
  • module:模块系统,推荐 ESNext
  • moduleResolution:推荐 bundler(打包器环境)
  • verbatimModuleSyntax:强制 import type 语法
  • skipLibCheck:跳过 .d.ts 文件检查,加速编译
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true
  }
}

追问noUncheckedIndexedAccess 有什么作用?(索引访问返回 T | undefined


33. strict 模式具体包含哪些子选项?

答案要点

strict: true 等价于同时开启以下所有选项:

  • strictNullChecksnull/undefined 不能赋值给其他类型
  • strictFunctionTypes:函数参数严格逆变检查
  • strictBindCallApplybind/call/apply 参数严格检查
  • strictPropertyInitialization:类属性必须初始化或在构造函数中赋值
  • noImplicitAny:禁止隐式 any
  • noImplicitThis:禁止隐式 this 类型为 any
  • useUnknownInCatchVariablescatcherror 参数类型为 unknown 而非 any
  • alwaysStrict:生成代码始终使用严格模式
typescript
// useUnknownInCatchVariables: true
try {
  throw new Error('fail');
} catch (e) {
  // e 是 unknown,必须收窄
  if (e instanceof Error) {
    console.log(e.message); // ✅
  }
}

追问exactOptionalPropertyTypesstrictNullChecks 的区别?


34. TypeScript 的编译流程是怎样的?tsc 做了什么?

答案要点

  • Scanner(扫描器):源码 → Token 流
  • Parser(解析器):Token → AST(抽象语法树)
  • Binder(绑定器):AST → Symbol Table(符号表),建立作用域
  • Checker(检查器):类型检查,最核心最耗时的阶段
  • Emitter(发射器):AST → JavaScript + .d.ts + SourceMap
text
源码(.ts) → Scanner → Parser → AST

                               Binder → Symbols

                               Checker → 类型检查

                               Emitter → .js + .d.ts + .map
  • 类型擦除:所有类型标注在编译后被移除,运行时不存在 TypeScript 类型信息
  • TS 5.8+ 的 --erasableSyntaxOnly 确保只使用可擦除语法

追问:为什么说 TypeScript 的 Checker 是最复杂的部分?(图灵完备的类型系统)


35. 项目引用(Project References)和增量编译是什么?

答案要点

  • 项目引用references 字段将大项目拆分为多个子 tsconfig,实现独立编译和缓存
  • 增量编译incremental: true + .tsbuildinfo 文件,只重新编译变更的部分
  • composite: true:启用项目引用的子项目必须开启
json
// tsconfig.json(根项目)
{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" },
    { "path": "./packages/api" }
  ]
}

// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "incremental": true,
    "declaration": true,
    "outDir": "./dist"
  }
}
  • 使用 tsc --buildtsc -b)编译,自动处理依赖顺序
  • Monorepo 项目中大幅提升编译速度

追问tsc --build 和直接 tsc 的区别?


TypeScript 5.x 新特性

36. TypeScript 5.0-5.7 带来了哪些重要新特性?

答案要点

TS 5.0

  • 装饰器(Decorators):符合 TC39 Stage 3 标准的新装饰器语法
  • const 类型参数<const T> 让泛型参数自动推断为字面量类型
  • moduleResolution: bundler:新的模块解析策略

TS 5.1

  • 函数返回 undefined 时可以不写 return 语句
  • getter/setter 可以有不同类型

TS 5.2

  • using 声明:确定性资源管理(Explicit Resource Management)
  • 装饰器元数据

TS 5.3

  • import 属性语法(import ... with { type: 'json' }
  • switch(true) 类型收窄

TS 5.4

  • NoInfer<T> 工具类型:阻止特定位置的类型推断
  • 闭包中的类型收窄改进

TS 5.5

  • 推断的类型谓词:自动推导 filter 回调为类型守卫
  • 正则表达式语法检查

TS 5.6

  • 迭代器辅助方法Iterator.map/filter 等)
  • --noUncheckedSideEffectImports

TS 5.7

  • --rewriteRelativeImportExtensions:自动重写导入路径扩展名
  • 初始化变量的类型收窄改进

追问using 声明和 try/finally 相比有什么优势?


37. const 类型参数(<const T>)是什么?解决了什么问题?

答案要点

  • 在泛型参数前加 const,让推断结果自动变为字面量/只读类型
  • 等价于调用方写 as const,但对调用方更友好
typescript
// 没有 const:推断为宽泛类型
function routes<T extends Record<string, string>>(config: T) {
  return config;
}
const r1 = routes({ home: '/', about: '/about' });
// T = { home: string; about: string }  ❌ 丢失字面量

// 有 const:自动推断为字面量
function routesConst<const T extends Record<string, string>>(config: T) {
  return config;
}
const r2 = routesConst({ home: '/', about: '/about' });
// T = { readonly home: "/"; readonly about: "/about" }  ✅ 保留字面量

追问<const T>satisfies 有什么异同?


38. using 声明(资源管理)是什么?怎么用?

答案要点

  • TC39 Stage 3 提案(Symbol.dispose / Symbol.asyncDispose),TS 5.2+ 支持
  • 类似 C# 的 using、Python 的 with、Go 的 defer
  • 作用域结束时自动调用 [Symbol.dispose](),确保资源释放
typescript
// 定义可释放资源
class DatabaseConnection implements Disposable {
  constructor(private url: string) {
    console.log(`Connected to ${url}`);
  }

  query(sql: string) {
    return []; // 模拟查询
  }

  [Symbol.dispose]() {
    console.log(`Disconnected from ${this.url}`);
  }
}

// 使用 using 声明
function fetchData() {
  using db = new DatabaseConnection('postgres://localhost/mydb');
  const data = db.query('SELECT * FROM users');
  return data;
  // 函数结束自动调用 db[Symbol.dispose]()
}

// 异步版本
class AsyncFileHandle implements AsyncDisposable {
  async [Symbol.asyncDispose]() {
    // 异步清理
  }
}

async function processFile() {
  await using handle = new AsyncFileHandle();
  // ...
  // 自动 await handle[Symbol.asyncDispose]()
}

追问usingtry/finally 相比有什么优势?(多资源场景更简洁,避免嵌套)


39. NoInfer<T> 工具类型解决什么问题?

答案要点

  • TS 5.4+ 内置,阻止 TypeScript 从某个位置推断泛型参数
  • 常用于:当一个泛型参数出现在多个位置时,控制推断优先级
typescript
// 问题:默认值位置也参与推断,导致类型过宽
function createState<T>(initial: T, defaultValue: T) {
  return { value: initial, default: defaultValue };
}
// T 被推断为 string,但我们想让 defaultValue 跟随 initial 的推断
createState('hello', 42); // ❌ 应该报错但可能不报错

// 解决:用 NoInfer 阻止 defaultValue 参与推断
function createStateBetter<T>(initial: T, defaultValue: NoInfer<T>) {
  return { value: initial, default: defaultValue };
}
createStateBetter('hello', 42); // ❌ number 不能赋值给 string ✅

// 常见场景:事件处理器
function on<T extends string>(event: T, callback: (data: NoInfer<T>) => void) {
  // T 只从 event 参数推断,callback 中的 T 不参与推断
}

追问NoInfer 的实现原理?(利用条件类型阻断推断链)


工程实践

40. TypeScript 项目中如何处理 any 类型?有哪些替代方案?

答案要点

  • 原则any 是类型系统的逃生舱,应尽量避免
  • 替代方案
    • unknown — 安全的顶层类型,需收窄后使用
    • 泛型 — 保持类型信息的同时实现灵活性
    • 函数重载 — 处理多种参数组合
    • satisfies — 既要检查又要保留推断
typescript
// ❌ 常见的 any 滥用
function parse(input: any): any {
  return JSON.parse(input);
}

// ✅ 用泛型 + unknown 替代
function parse<T>(input: string): T {
  return JSON.parse(input) as T;
}

// ✅ 更安全:运行时校验(配合 zod)
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

type User = z.infer<typeof UserSchema>;

function parseUser(input: string): User {
  return UserSchema.parse(JSON.parse(input));
}
  • ESLint 配置@typescript-eslint/no-explicit-any 配合 @typescript-eslint/no-unsafe-* 系列规则

追问// @ts-ignore// @ts-expect-error 的区别?


41. TypeScript 与 Zod/Valibot 等运行时校验库如何配合?

答案要点

  • TypeScript 类型只在编译期存在,运行时被擦除,无法校验 API 返回值
  • 运行时校验库(Zod、Valibot、ArkType)解决了编译期与运行时的类型同步问题
  • 核心模式:Schema → 推导出 TypeScript 类型,一处定义两处使用
typescript
import { z } from 'zod';

// 1. 定义 Schema(单一来源)
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.coerce.date(),
});

// 2. 推导 TypeScript 类型
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "user" | "guest"; createdAt: Date }

// 3. 运行时安全解析
async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return UserSchema.parse(data); // 运行时校验,失败抛异常
}

// 4. Valibot(更轻量的替代方案,tree-shakable)
import * as v from 'valibot';

const UserSchema2 = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
});

追问:Zod 和 Valibot 的核心区别?(包体积、API 设计、tree-shaking)


42. 如何在大型项目中组织 TypeScript 类型?

答案要点

  • 按功能模块组织,而非集中在一个 types/ 目录
  • 类型就近原则:类型定义靠近使用处
  • 共享类型放在 shared/types@types/
text
src/
├── features/
│   ├── user/
│   │   ├── types.ts          # User 相关类型
│   │   ├── api.ts
│   │   └── components/
│   └── order/
│       ├── types.ts          # Order 相关类型
│       └── api.ts
├── shared/
│   ├── types/
│   │   ├── api.ts            # 通用 API 类型(Response、Pagination)
│   │   └── common.ts         # 通用工具类型
│   └── utils/
└── types/
    └── env.d.ts              # 环境变量、全局声明
  • 导出策略
    • 模块内部类型不导出(type 不加 export
    • 跨模块共享类型通过 barrel file(index.ts)统一导出
    • 使用 import type 明确区分类型导入和值导入

追问type 导出和 interface 导出在打包器中的区别?


43. TypeScript 中如何实现类型安全的 API 层?

答案要点

  • 使用泛型封装请求函数,实现端到端类型安全
  • 配合路由定义,确保请求参数和响应类型匹配
typescript
// 定义 API 路由类型映射
interface ApiRoutes {
  'GET /users': {
    query: { page: number; limit: number };
    response: { users: User[]; total: number };
  };
  'GET /users/:id': {
    params: { id: string };
    response: User;
  };
  'POST /users': {
    body: CreateUserInput;
    response: User;
  };
}

// 类型安全的请求函数
async function api<T extends keyof ApiRoutes>(
  route: T,
  config: Omit<ApiRoutes[T], 'response'>
): Promise<ApiRoutes[T]['response']> {
  // 实现...
  const [method, path] = (route as string).split(' ');
  const res = await fetch(path, { method });
  return res.json();
}

// 使用时完全类型安全
const { users, total } = await api('GET /users', {
  query: { page: 1, limit: 10 },
});
// users: User[], total: number ✅

追问:tRPC 是如何实现端到端类型安全的?


44. 装饰器(Decorators)在 TypeScript 5.x 中有什么变化?

答案要点

  • TS 5.0 实现了 TC39 Stage 3 标准装饰器,与旧版实验性装饰器(experimentalDecorators)不同
  • 新装饰器是纯函数,接收 (target, context) 两个参数
  • 不再需要 reflect-metadata,语义更清晰
typescript
// 新标准装饰器(TS 5.0+)
function log<T extends (...args: any[]) => any>(
  target: T,
  context: ClassMethodDecoratorContext
) {
  return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
    console.log(`Calling ${String(context.name)} with`, args);
    const result = target.apply(this, args);
    console.log(`Result:`, result);
    return result;
  } as T;
}

class Calculator {
  @log
  add(a: number, b: number) {
    return a + b;
  }
}

// 类装饰器
function sealed(target: Function, _context: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
}

@sealed
class Greeter {
  greet() { return 'Hello'; }
}

追问:新装饰器和旧版 experimentalDecorators 能共存吗?(不能,需要迁移)


45. TypeScript 性能优化有哪些技巧?

答案要点

  • 类型层面

    • 避免过度复杂的条件类型和递归类型
    • 使用 interface 而非 type(interface 有缓存优化)
    • 减少联合类型的成员数量
    • 避免 keyof 大型类型
  • 编译层面

    • 开启 incrementaltsBuildInfo
    • 使用项目引用拆分大项目
    • skipLibCheck: true 跳过第三方类型检查
  • 工具层面

    • 使用 @typescript-eslint/parser 替代 TSLint(已废弃)
    • 考虑 SWC / esbuild 做转译(只做类型擦除,不做类型检查)
    • tsc --noEmit 只做类型检查,转译交给更快的工具
bash
# 分析 TypeScript 编译性能
tsc --generateTrace ./trace
# 用 Chrome DevTools 的 Performance 面板打开 trace 文件分析

# 只做类型检查(CI 中常用)
tsc --noEmit

# 用 SWC 做快速转译
npx swc src -d dist

追问:为什么 interface extendstype & 交叉类型性能更好?