interface & type
两者都能定义对象结构和函数签名,但interface 更偏向结构扩展与 OOP 语义;type 更偏向类型组合与类型表达式。
在工程中,interface 用于描述数据结构,type 用于类型运算和复杂组合。
相同点
- 都能描述对象形状
- 都能做函数类型定义
- 都支持泛型
- 都能被 class implements
interface A { x: number }
type B = { x: number }
核心区别
- interface 可以合并声明
interface User { id: number }
interface User { name: string }
const u: User = { id: 1, name: 'a' }
👉 type 不行,这是 interface 最重要的能力。
适合用来扩展第三方库定义(例如扩展 Express Request)。
- type 更灵活,支持类型表达式
type 可以:
- 联合
- 交叉
- 条件类型
- 映射类型
- 模板字符串类型
type Status = 'success' | 'error';
type WithId<T> = T & { id: string };
type Api<T> = T extends any[] ? 'array' : 'object';
👉 interface 做不到这些。
- interface 更适合建模“对象、类、API”结构
interface Person {
name: string;
say(): void;
}
它能:
- 扩展其他 interface(extends)
- 被类 implements(非常 OOP)
- type 可以“别名化”(alias)任何类型
type Fn = (x: number) => void;
type Point = [number, number];
type Maybe<T> = T | undefined;
👉 type 的使用范围更广,甚至可以 alias 基本类型。
interface 做不到。
- 扩展方式不同(extends vs 交叉类型)
- interface 扩展
interface A { x: number }
interface B extends A { y: number }
- type 扩展
type A = { x: number }
type B = A & { y: number }
两者效果一致,但交叉类型能做更多复杂组合。
- 复杂类型构造:type 更强
前端工程里的工具类型、复杂泛型、联合类型转换 几乎都是 type 实现的。
type Partial<T> = {
[P in keyof T]?: T[P];
}
interface 无法实现这种类型运算。
工程中如何选择
如果是 描述数据结构、业务对象、class API,优先用 interface,因为其更可扩展、可合并,团队协作更友好。
如果是 类型组合、联合类型、工具类型、映射类型、模板字面量,一定用 type,因为更灵活。
unknow & any & never
🐱unknown
unknown 是安全的 any,可以接受任何值,但不能被直接使用,需要类型收窄。适用于以下的严格使用场景。
- 不信任来源的数据(API、用户输入、动态数据)
function parse(json: string): unknown {
return JSON.parse(json);
}
- 泛型上界不确定,但不希望失去类型安全
function handle<T extends unknown>(value: T) {}
- 设计库时需要“类型安全的扩展点”
比如写 SDK、插件系统时,unknown 让调用方必须显式确认类型。
🐶never
never 代表“不可能发生的值”(类型系统的底部类型)
- 程序不会走到这里
- 函数不会返回
- 分支被完全排除
适用于以下的严格使用场景。
- 表示函数永远不返回(异常 / 死循环)
function fail(msg: string): never {
throw new Error(msg);
}
- 类型收窄后出现“不可能的情况”
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
配合基础类型保护写 exhaustiveness check:
type Shape = 'circle' | 'square';
function area(s: Shape) {
switch (s) {
case 'circle': return 1;
case 'square': return 1;
default:
return assertNever(s); // 编译期报错,保证分支覆盖完整
}
}
- 联合类型排除成员
type Exclude<T, U> = T extends U ? never : T;
“never 用于严格的编译期检查、不可达代码、以及保证联合类型逻辑完备性。”
🐯any
any 会关闭所有类型检查,是 TS 世界的“丧失类型安全”。适用于以下严格使用场景(只有这些情况能用)
- 渐进式迁移 JS → TS,需要暂时兜底
let temp: any = legacyLib.getData();
- 类型真的无法确定(第三方库,动态字段特别复杂)
比如老旧 SDK,或者大量动态 key。
- 需要与 JS 环境交互(特殊全局变量、动态属性)
例如 window 全局写入自定义字段。
- 为了避免过度复杂的类型推导(工程权衡)
例如复杂泛型导致 IDE 卡顿。
any 是一种工程妥协,不是类型,它是关闭类型检查的开关。只有在迁移、兼容、动态环境时才应该使用。
三者关系
- unknown 是安全的顶类型(不信任输入 → 先 unknown)
- never 是不可能发生的底类型(穷尽检查、异常、严格逻辑)
- any 是逃生舱(完全关闭类型检查,只在必要时使用)
unknown 与 any 是对立的:一个提高安全,一个降低安全;
never 则代表类型系统的底部,是逻辑完备性检查的核心工具。
Partial & Pick & infer
Partial 和 Pick 是典型的映射类型,用来‘改造’对象键;
infer 是在条件类型中做‘反向推断’,固定在提取某些类型信息的场景里。
这三个是 TypeScript 类型系统里最基础也最常用的构建块,理解它们能写出真正类型安全的库级代码。”
🍒Partial
Partial<T> 会把类型 T 的所有属性变成可选属性。
它是一个典型的「映射类型」,通过遍历 keyof T,把每个键的属性加上 ? 修饰符。
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
工程使用场景:
- 表单场景:编辑时部分字段可选
- DTO/Object patch:只更新传入字段
- 配置对象:允许用户覆盖默认配置
生产里基本把所有配置类型都写成 Partial 形式,减少冗余类型声明,也避免用户必须传所有属性。
🍑Pick
Pick<T, K> 从类型 T 中挑选部分键 K,生成一个子类型。
它同样是映射类型,只是遍历 K 而不是 keyof T。
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
工程使用场景
- API 拆分:只需要某些字段
- React Props 提取
- 二次封装组件时抽取部分属性
Pick 本质上就是结构化编程里的投影操作,非常常用。
🍇infer
infer 是 TypeScript 在条件类型里用于声明一个待推断的类型变量。
它让 TypeScript 能“从类型中反向提取信息”。
- 只能在条件类型
T extends X ? ... : ...的true分支里使用 - 相当于告诉 TS:“这里有个未知类型 R,你帮我推断它是什么”
- 最典型的例子:从函数类型中提取返回值
手写 ReturnType
type MyReturnType<F> =
F extends (...args: any[]) => infer R
? R
: never;
常见 infer 使用:
- 提取 Promise 内部类型
type Awaited<T> = T extends Promise<infer R> ? R : T;
- 提取数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : T;
工程使用场景
- 自动根据 API 类型生成 Response 类型
- 根据函数库自动推断返回值
- 在前端状态管理中自动生成 Action 类型
- 在复杂表单里从 DTO 推类型
infer 是 TypeScript 高阶类型里很核心的机制,它让 TypeScript 能像编译器一样进行模式匹配。