2026-01-04 21:19:13 +08:00

241 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TypeScript 深度面试题及解析
> 本文档汇集了高频且具有深度的 TypeScript 面试题,旨在帮助开发者掌握 TS 类型系统的核心原理及高级应用。
## 目录
1. [基础概念与类型系统](#1-基础概念与类型系统)
2. [高级类型与类型体操](#2-高级类型与类型体操)
3. [TS 模块与工程化](#3-ts-模块与工程化)
4. [React/Vue 实战场景](#4-reactvue-实战场景)
---
## 1. 基础概念与类型系统
### Q1: `interface` 和 `type` (Type Alias) 的核心区别是什么?在什么场景下应该优先使用哪一个?
**解析:**
虽然两者在很多场景下可以互换,但设计初衷不同。
* **Interface (接口)**:主要用于定义**对象**的形状Shape和类的契约。它支持**声明合并 (Declaration Merging)**,这对于扩展第三方库的类型非常重要。
* **Type (类型别名)**:不仅仅是对象,它可以定义基本类型别名、联合类型 (Union)、元组 (Tuple) 等。它**不支持**声明合并。
**深度对比:**
1. **扩展方式**`interface` 使用 `extends``type` 使用交叉类型 `&`
2. **声明合并**:同名的 `interface` 会自动合并,同名的 `type` 会报错。
3. **索引签名**`type` 在某些复杂的映射类型中表现更好,而 `interface` 在索引签名上有时会有限制(例如不能直接映射非对象类型)。
**最佳实践:**
* **编写库或公共 API 时**,优先使用 `interface`,以便使用者可以通过声明合并扩展类型。
* **定义联合类型、复杂的工具类型、提取类型时**,必须使用 `type`
* **一般应用开发中**,保持一致即可,`type` 因其灵活性目前更受欢迎。
### Q2: 解释 TypeScript 中的 `any`、`unknown` 和 `never` 的区别
**解析:**
这是考察对类型安全理解深度的经典问题。
* **`any` (任意类型)**
* **含义**:关闭类型检查。任何类型都能赋值给 `any``any` 也能赋值给任何类型。
* **风险**:使用 `any` 会失去 TS 的保护,像是在写普通的 JS。应尽量避免。
* **`unknown` (未知类型)**
* **含义**:类型安全的 `any`。任何类型都能赋值给 `unknown`,但 `unknown` **不能**直接赋值给其他类型(除了 `any``unknown`),也不能直接调用其方法。
* **使用**:必须先进行**类型收窄 (Type Narrowing)**(如 `typeof`, `instanceof`, 类型断言)才能使用。
* **`never` (永不存在的类型)**
* **含义**:表示那些永远不存在的值的类型。例如:抛出异常的函数返回值、无限循环的函数返回值、联合类型中被排除后的剩余类型。
* **特性**`never` 是所有类型的子类型Bottom Type可以赋值给任何类型但没有类型可以赋值给 `never`
* **应用**:常用于**全面性检查 (Exhaustiveness Checking)**。
**代码示例:全面性检查**
```typescript
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)。
```typescript
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) 中声明一个类型变量,用于**推断**类型。
```typescript
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 的实现**
```typescript
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`
**示例:自定义类型谓词**
```typescript
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. **组件静态属性**`defaultProps``React.FC` 下处理较为复杂。
**推荐写法:**
```typescript
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)**:
```typescript
const props = defineProps({
foo: { type: String, required: true }
})
```
* 优点:运行时有校验。
* 缺点TS 类型推导不如纯类型声明直接。
2. **基于类型的声明 (Type-based Declaration) - 推荐**:
```typescript
const props = defineProps<{
foo: string
bar?: number
}>()
```
* 优点:纯 TS 语法DX开发体验更好支持复杂类型。
* 注意:`withDefaults` 宏用于设置默认值。
---
> **总结**TypeScript 面试的核心在于理解**类型系统是图灵完备的**(可以通过类型编程解决复杂问题),以及理解**编译时类型**与**运行时代码**的边界Erasure