241 lines
8.9 KiB
Markdown
241 lines
8.9 KiB
Markdown
# 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)。
|