2026-01-04 23:26:58 +08:00

9.7 KiB
Raw Blame History

TypeScript 深度面试题及解析

本文档汇集了高频且具有深度的 TypeScript 面试题,旨在帮助开发者掌握 TS 类型系统的核心原理及高级应用。

目录

  1. 基础概念与类型系统
  2. 高级类型与类型体操
  3. TS 模块与工程化
  4. React/Vue 实战场景

1. 基础概念与类型系统

在深入面试题之前,我们先快速梳理一下 TypeScript 的类型系统版图:

  • 基础类型 (Primitives)string, number, boolean, null, undefined, symbol, bigint
  • 顶层类型 (Top Types)
    • any:关闭类型检查,尽量避免
    • unknown:类型安全的 any使用前需类型收窄
  • 底层类型 (Bottom Type)
    • never:不可能出现的值(如抛错/死循环的返回)
  • 复合类型 (Composed Types)
    • 联合类型 (Union Types)string | number(是 A 或 B
    • 交叉类型 (Intersection Types)TypeA & TypeB(同时满足 A 与 B
    • 元组 (Tuple):定长定类型数组,如 [string, number]
  • 对象类型:interfacetype(对象字面量、映射类型)、class、字面量类型 { name: string }

Q1: interfacetype (Type Alias) 的核心区别是什么?在什么场景下应该优先使用哪一个?

解析: 虽然两者在很多场景下可以互换,但设计初衷不同。

  • Interface (接口):主要用于定义对象的形状Shape和类的契约。它支持声明合并 (Declaration Merging),这对于扩展第三方库的类型非常重要。
  • Type (类型别名):不仅仅是对象,它可以定义基本类型别名、联合类型 (Union)、元组 (Tuple) 等。它不支持声明合并。

深度对比:

  1. 扩展方式interface 使用 extendstype 使用交叉类型 &
  2. 声明合并:同名的 interface 会自动合并,同名的 type 会报错。
  3. 索引签名type 在某些复杂的映射类型中表现更好,而 interface 在索引签名上有时会有限制(例如不能直接映射非对象类型)。

最佳实践:

  • 编写库或公共 API 时,优先使用 interface,以便使用者可以通过声明合并扩展类型。
  • 定义联合类型、复杂的工具类型、提取类型时,必须使用 type
  • 一般应用开发中,保持一致即可,type 因其灵活性目前更受欢迎。

Q2: 解释 TypeScript 中的 anyunknownnever 的区别

解析: 这是考察对类型安全理解深度的经典问题。

  • any (任意类型)
    • 含义:关闭类型检查。任何类型都能赋值给 anyany 也能赋值给任何类型。
    • 风险:使用 any 会失去 TS 的保护,像是在写普通的 JS。应尽量避免。
  • unknown (未知类型)
    • 含义:类型安全的 any。任何类型都能赋值给 unknown,但 unknown 不能直接赋值给其他类型(除了 anyunknown),也不能直接调用其方法。
    • 使用:必须先进行类型收窄 (Type Narrowing)(如 typeof, instanceof, 类型断言)才能使用。
  • never (永不存在的类型)
    • 含义:表示那些永远不存在的值的类型。例如:抛出异常的函数返回值、无限循环的函数返回值、联合类型中被排除后的剩余类型。
    • 特性never 是所有类型的子类型Bottom Type可以赋值给任何类型但没有类型可以赋值给 never
    • 应用:常用于全面性检查 (Exhaustiveness Checking)

代码示例:全面性检查

type Shape = 'circle' | 'square';

function getArea(shape: Shape) {
  switch (shape) {
    case 'circle': return Math.PI;
    case 'square': return 1;
    default:
      // 如果 Shape 扩展了 'triangle',这里会报错,因为 'triangle' 不能赋值给 never
      const _exhaustiveCheck: never = shape; 
      return _exhaustiveCheck;
  }
}

Q3: 什么是协变 (Covariance) 和逆变 (Contravariance)?在 TS 中如何体现?

解析: 这是类型系统中非常底层的概念,主要体现在函数类型的兼容性上。

  • 协变 (Covariance):允许子类型赋值给父类型。
    • 体现对象属性函数返回值
    • 如果 Dog extends Animal,那么 () => Dog 可以赋值给 () => Animal
  • 逆变 (Contravariance):允许父类型赋值给子类型(看起来是反直觉的)。
    • 体现函数参数(在开启 strictFunctionTypes 时)。
    • 如果 Dog extends Animal,那么 (animal: Animal) => void 可以赋值给 (dog: Dog) => void
    • 理解:如果一个函数能处理所有的 Animal,那它一定能处理 Dog。反之,如果一个函数只能处理 Dog,你传给它一个 Cat (也是 Animal) 它就崩了。

口诀:

参数逆变,返回值协变。


2. 高级类型与类型体操

Q4: 如何实现一个 DeepReadonly<T> 工具类型?

解析: 考察递归类型和映射类型 (Mapped Types)。

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object 
    ? (T[P] extends Function ? T[P] : DeepReadonly<T[P]>) // 处理函数和对象的递归
    : T[P];
};

注意:实际面试中,简单的递归对象即可,处理 Function 的情况属于加分项。

Q5: 解释 infer 关键字的作用,并写一个提取 Promise 返回值的类型 UnwrapPromise<T>

解析: infer 用于在条件类型 (Conditional Types) 中声明一个类型变量,用于推断类型。

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

// 进阶:处理嵌套 Promise (递归)
type DeepUnwrapPromise<T> = T extends Promise<infer U> 
  ? DeepUnwrapPromise<U> 
  : T;

Q6: 什么是分布式条件类型 (Distributive Conditional Types)

解析: 当条件类型作用于泛型,并且传入的是联合类型时,条件类型会分发Distribute执行。

公式: (A | B) extends T ? X : Y
=> (A extends T ? X : Y) | (B extends T ? X : Y)

应用Exclude 的实现

type MyExclude<T, U> = T extends U ? never : T;

type Result = MyExclude<'a' | 'b' | 'c', 'a'>; 
// 过程:
// ('a' extends 'a' ? never : 'a') | 
// ('b' extends 'a' ? never : 'b') | 
// ('c' extends 'a' ? never : 'c')
// => never | 'b' | 'c' 
// => 'b' | 'c'

如何阻止分发? 用元组包裹:[T] extends [U] ? X : Y


3. TS 模块与工程化

Q7: const enum 和普通 enum 有什么区别?

解析:

  • 普通 enum
    • 运行时存在。会编译成一个双向映射的 JavaScript 对象(如果是数字枚举)。
    • 优点:可以反向查找(值 -> 键)。
    • 缺点:增加 bundle 体积。
  • const enum (常量枚举)
    • 运行时不存在。在编译阶段会被内联Inlined替换为具体的值。
    • 优点:零运行时开销,体积小。
    • 缺点:不能反向查找;在某些构建工具(如 Babel 早期版本)或 isolatedModules 模式下可能存在兼容性问题(因为没有运行时对象)。

Q8: 什么是类型守卫 (Type Guard)?有哪些方式可以实现?

解析: 类型守卫用于在运行时检查类型,从而在特定作用域内收窄类型。

  1. typeof: 识别基础类型 (string, number 等)。
  2. instanceof: 识别类实例。
  3. in 关键字: 检查对象是否包含某属性。
  4. 自定义类型谓词 (Type Predicate): parameter is Type

示例:自定义类型谓词

interface Fish { swim: () => void; }
interface Bird { fly: () => void; }

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// 使用
if (isFish(pet)) {
  pet.swim(); // TS 知道这里 pet 是 Fish
}

4. React/Vue 实战场景

Q9: 在 React 中,React.FC 还需要用吗?为什么现在不推荐了?

解析: 在 React 18 之前,React.FC (FunctionComponent) 曾是定义组件的标准方式,但现在社区(包括 Create React App 模板)倾向于直接声明函数参数类型。

React.FC 的问题(旧版):

  1. 隐式 children:旧版 React.FC 默认包含了 children 属性,即使你的组件不需要 children这破坏了类型严格性。
  2. 泛型支持差:给 React.FC 组件添加泛型比较麻烦。
  3. 组件静态属性defaultPropsReact.FC 下处理较为复杂。

推荐写法:

interface Props {
  title: string;
  children?: React.ReactNode; // 显式声明 children
}

const MyComponent = ({ title, children }: Props) => {
  return <div>{title}{children}</div>;
};

Q10: Vue 3 中 defineProps 如何结合 TypeScript 使用?运行时声明 vs 类型声明?

解析: Vue 3 提供了两种方式定义 props

  1. 运行时声明 (Runtime Declaration):

    const props = defineProps({
      foo: { type: String, required: true }
    })
    
    • 优点:运行时有校验。
    • 缺点TS 类型推导不如纯类型声明直接。
  2. 基于类型的声明 (Type-based Declaration) - 推荐:

    const props = defineProps<{
      foo: string
      bar?: number
    }>()
    
    • 优点:纯 TS 语法DX开发体验更好支持复杂类型。
    • 注意:withDefaults 宏用于设置默认值。

总结TypeScript 面试的核心在于理解类型系统是图灵完备的(可以通过类型编程解决复杂问题),以及理解编译时类型运行时代码的边界Erasure