8.9 KiB
TypeScript 深度面试题及解析
本文档汇集了高频且具有深度的 TypeScript 面试题,旨在帮助开发者掌握 TS 类型系统的核心原理及高级应用。
目录
1. 基础概念与类型系统
Q1: interface 和 type (Type Alias) 的核心区别是什么?在什么场景下应该优先使用哪一个?
解析: 虽然两者在很多场景下可以互换,但设计初衷不同。
- Interface (接口):主要用于定义对象的形状(Shape)和类的契约。它支持声明合并 (Declaration Merging),这对于扩展第三方库的类型非常重要。
- Type (类型别名):不仅仅是对象,它可以定义基本类型别名、联合类型 (Union)、元组 (Tuple) 等。它不支持声明合并。
深度对比:
- 扩展方式:
interface使用extends,type使用交叉类型&。 - 声明合并:同名的
interface会自动合并,同名的type会报错。 - 索引签名:
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)。
代码示例:全面性检查
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)?有哪些方式可以实现?
解析: 类型守卫用于在运行时检查类型,从而在特定作用域内收窄类型。
typeof: 识别基础类型 (string,number等)。instanceof: 识别类实例。in关键字: 检查对象是否包含某属性。- 自定义类型谓词 (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 的问题(旧版):
- 隐式
children:旧版React.FC默认包含了children属性,即使你的组件不需要 children,这破坏了类型严格性。 - 泛型支持差:给
React.FC组件添加泛型比较麻烦。 - 组件静态属性:
defaultProps在React.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:
-
运行时声明 (Runtime Declaration):
const props = defineProps({ foo: { type: String, required: true } })- 优点:运行时有校验。
- 缺点:TS 类型推导不如纯类型声明直接。
-
基于类型的声明 (Type-based Declaration) - 推荐:
const props = defineProps<{ foo: string bar?: number }>()- 优点:纯 TS 语法,DX(开发体验)更好,支持复杂类型。
- 注意:
withDefaults宏用于设置默认值。
总结:TypeScript 面试的核心在于理解类型系统是图灵完备的(可以通过类型编程解决复杂问题),以及理解编译时类型与运行时代码的边界(Erasure)。