TypeScript 接口 vs 类型别名 — 一次讲透,到底用哪个?
在 TypeScript 中定义对象类型,你有两种选择:
interface和type。它们长得像、功能也像,到底该用哪个?本文从语法、扩展、合并、独有能力四个维度,彻底讲透两者的区别与选择策略。
目录
一、两种方式,一个目标
在 TypeScript 中,当你需要描述一个对象的形状时,有两种语法可用:
// 方式一:interface
interface User {
name: string;
age: number;
}
// 方式二:type alias(类型别名)
type User = {
name: string;
age: number;
};
两者都能描述"一个对象有 name 属性(string 类型)和 age 属性(number 类型)"。
那为什么要有两种?它们的区别到底在哪? 这是 TypeScript 初学者最常见的问题之一。本文从以下维度彻底讲透:
二、基本语法对比
2.1 interface 基本语法
// 用 interface 关键字声明,不需要等号
interface User {
name: string;
age: number;
readonly id: number; // 只读属性
email?: string; // 可选属性
}
const user: User = {
name: "张三",
age: 28,
id: 1001,
// email 可不写
};
2.2 type 基本语法
// 用 type 关键字声明,需要等号赋值
type User = {
name: string;
age: number;
readonly id: number; // 只读属性
email?: string; // 可选属性
};
const user: User = {
name: "张三",
age: 28,
id: 1001,
};
2.3 这一层的结论
在描述对象形状这件事上,两者几乎完全等价:
都能定义普通属性
都能定义可选属性(
?)都能定义只读属性(
readonly)都能定义索引签名
编译后都会被擦除,不占运行时
// 编译前(TS)
interface User { name: string; }
type Product = { name: string };
// 编译后(JS)—— 两者都消失了,不占任何运行时
// (没有任何 JS 代码输出)
三、基础用法逐项 PK
3.1 描述函数类型
// interface:使用 call signature(调用签名)
interface Greet {
(name: string): string;
}
// type:直接用箭头函数语法(更简洁)
type Greet = (name: string) => string;
结论:描述函数类型时,type 的箭头函数语法更直观、更简洁。
3.2 索引签名
// interface
interface StringMap {
[key: string]: string;
}
// type
type StringMap = {
[key: string]: string;
};
// 两者完全等价
3.3 联合类型 — 第一个分水岭
// ✅ type 可以定义联合类型
type Status = "active" | "inactive" | "pending";
type ID = string | number;
// ❌ interface 不能定义联合类型
interface Status = "active" | "inactive"; // Error!
这是两者的第一个本质区别:type 能表达联合类型,interface 不能。联合类型是 TypeScript 类型系统的核心能力之一,后面会大量用到。
3.4 基础用法对比表
四、扩展方式对比
当一个类型需要基于另一个类型进行扩展时,两者的写法不同。
4.1 interface 用 extends 继承
// 基础接口
interface Animal {
name: string;
}
// extends 继承,支持多继承
interface Dog extends Animal {
breed: string;
}
// 多继承
interface Pet extends Animal, Serializable {
owner: string;
}
4.2 type 用交叉类型 & 拼接
// 基础类型
type Animal = {
name: string;
};
// 用 & 拼接扩展
type Dog = Animal & {
breed: string;
};
// 多个类型拼接
type Pet = Animal & Serializable & {
owner: string;
};
4.3 扩展方式对比
五、声明合并 — interface 的特权
这是 interface 独有的能力,type 做不到。
5.1 什么是声明合并?
当两个同名的 interface 出现时,TypeScript 会自动把它们合并成一个:
// 第一次声明
interface Window {
myProp: string;
}
// 第二次声明(同名)
interface Window {
newProp: number;
}
// 合并后的效果:
// interface Window {
// myProp: string;
// newProp: number;
// }
// 使用
const w = window as Window;
w.myProp; // string
w.newProp; // number
5.2 声明合并的实战场景
场景:扩展第三方库的类型
// 第三方库定义了 Express.Request 接口
// 你想给它增加自定义字段
import { Request } from "express";
// 声明合并:给 Request 添加用户字段
declare module "express" {
interface Request {
user: {
id: string;
name: string;
};
}
}
// 现在在所有路由中都能用了
app.get("/profile", (req: Request, res) => {
console.log(req.user.id); // ✅ 类型安全
});
这种场景用 type 做不到:
// ❌ type 同名会直接报错
type Foo = { a: string };
type Foo = { b: number };
// Error: Duplicate identifier 'Foo'.
5.3 合并规则
记住:声明合并是 interface 的特权。当你需要扩展已有类型时,interface 是唯一选择。
六、type 的四大独门绝技
反过来,type 也有 interface 做不到的事。以下是 type 独有的四大能力:
6.1 联合类型(Union Types)
一个值可以是多种类型之一:
type Status = "active" | "inactive" | "pending";
type ID = string | number;
type Result = Success | Error; // 联合多个对象类型
// interface 做不到
// interface Status = "active" | "inactive"; // ❌ Error
6.2 元组类型(Tuple Types)
固定长度和每个位置类型的数组:
type Coordinate = [number, number]; // 经纬度
type HttpResponse = [number, string]; // [状态码, 响应体]
type RGB = [number, number, number]; // 颜色值
// interface 做不到
// interface Coordinate = [number, number]; // ❌ Error
6.3 映射类型(Mapped Types)
用 in 关键字遍历键,批量生成类型:
// 把所有属性变成只读
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// 把所有属性变成可选
type Partial<T> = {
[K in keyof T]?: T[K];
};
// interface 做不到
// interface Readonly<T> { readonly [K in keyof T]: T[K] }; // ❌ Error
6.4 条件类型(Conditional Types)
根据类型判断返回不同类型:
// 判断 T 是否是 string 的子类型
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
// interface 做不到
// interface IsString<T> = T extends string ? true : false; // ❌ Error
6.5 四大能力总结
结论:type 是真正的类型编程工具,能做复杂的类型计算;interface 只能描述对象形状。
七、同名冲突的不同表现
当扩展类型时出现同名属性但类型不同,两者的处理方式截然不同。
7.1 interface:报错提示
interface A {
x: string;
}
interface A {
// ❌ Error: Property 'x' of type 'number' is not assignable to 'string'.
x: number;
}
好处:TypeScript 明确告诉你哪里冲突了,方便定位问题。
7.2 type:静默变成 never
type A = { x: string };
type B = { x: number };
type C = A & B;
// C 的 x 属性变成了 never —— 静默失败!
const obj: C = {
x: undefined as never, // 只能赋 never 值
};
危险之处:没有报错提示,x 变成了 never 类型——一个永远无法赋值的类型。这在实际开发中很难发现。
7.3 对比
八、到底怎么选?三条决策规则
不要纠结,记住这三条规则就够了:
规则一:描述对象形状 → 用 interface
// ✅ 用 interface
interface User {
name: string;
age: number;
}
interface Product {
id: number;
price: number;
}
适用场景:
描述 API 响应结构
描述组件 Props
描述配置对象
需要声明合并
扩展第三方库类型
规则二:需要类型计算 → 用 type
// ✅ 用 type
type Status = "active" | "inactive"; // 联合
type Point = [number, number]; // 元组
type ReadonlyUser = Readonly<User>; // 映射
type IsString<T> = T extends string ? true : false; // 条件
适用场景:
联合类型
元组类型
映射类型
条件类型
工具类型(Utility Types)
规则三:不确定 → 默认用 interface
// 不确定时,先用 interface
interface Todo {
id: number;
title: string;
done: boolean;
}
// 等到需要联合/映射/条件时,再切换到 type
type TodoFilter = "all" | "active" | "completed";
原因:
interface更直观,语法更接近面向对象报错信息更友好(冲突时报错而非变
never)支持声明合并,扩展性更好
这是社区的主流实践
九、类比记忆:合同 vs 公式
用一句话记住两者的本质区别:
interface 是合同 — 双方签字后可以追加条款,但不能有矛盾的条款。
type 是公式 — 定义后不能改,但可以组合、变形、嵌套。
详细类比
十、常见坑点与最佳实践
坑点1:type 交叉类型冲突变 never
type A = { x: string };
type B = { x: number };
type C = A & B; // x: never —— 静默失败!
// 更好的做法:用 interface 让冲突暴露
interface A { x: string; }
interface B extends A { x: number; } // ❌ 报错!明确告诉你冲突了
坑点2:混淆 extends 和 &
// interface extends:冲突会报错
interface A { x: string; }
interface B extends A { x: number; } // ❌ Error
// type & :冲突变 never
type A = { x: string; };
type B = A & { x: number; }; // B.x: never,不报错
坑点3:以为 type 也能声明合并
// ❌ type 不能声明合并
type Foo = { a: string };
type Foo = { b: number }; // Error: Duplicate identifier 'Foo'
// ✅ 只有 interface 能声明合并
interface Bar { a: string; }
interface Bar { b: number; } // ✅ 自动合并
坑点4:interface 不能给基本类型起别名
// ✅ type 可以
type Name = string;
type Callback = () => void;
// ❌ interface 不能描述基本类型
interface Name = string; // Error!
最佳实践清单
默认用
interface描述对象形状:更直观、报错更友好需要联合/元组/映射/条件时用
type:这些是 type 的专属能力扩展第三方库类型用
interface+declare module:声明合并是唯一方式避免在
type中制造隐式never:同名冲突用interface暴露问题不要混用两种风格:同一个项目内保持一致
函数类型用
type:箭头函数语法更简洁React 组件 Props 用
interface:社区惯例,且支持扩展
十一、自测挑战
挑战一:以下代码哪些会报错?
// A
interface A { x: string; }
interface A { x: number; }
// B
type B = { x: string; };
type B = { x: number; };
// C
interface C { x: string; }
interface CC extends C { y: number; }
// D
type D = string | number;
// E
interface E = string | number;
点击展开答案
A:❌ 报错。interface 同名属性类型不同会报错。
B:❌ 报错。type 同名会报 Duplicate identifier。
C:✅ 正确。interface extends 扩展是合法的。
D:✅ 正确。type 支持联合类型。
E:❌ 报错。interface 不支持联合类型,也不需要等号。
挑战二:用两种方式实现以下类型
要求:定义一个 User 类型,包含 name(string)、age(number)、email(可选 string),然后扩展出 AdminUser,增加 permissions(string 数组)。
点击展开答案
// interface 方式
interface User {
name: string;
age: number;
email?: string;
}
interface AdminUser extends User {
permissions: string[];
}
// type 方式
type User = {
name: string;
age: number;
email?: string;
};
type AdminUser = User & {
permissions: string[];
};
挑战三:以下代码的 x 是什么类型?
type A = { x: string };
type B = { x: number };
type C = A & B;
点击展开答案
C.x 的类型是 never。因为 string & number 没有交集,交叉类型会变成 never。这是 type 交叉类型的隐式失败行为,不会报错但很危险。
十二、总结与知识图谱
知识图谱
interface vs type
│
├── 共同能力(两者等价)
│ ├── 描述对象形状
│ ├── 可选属性 ?
│ ├── 只读属性 readonly
│ └── 索引签名 [key: T]
│
├── interface 独有
│ ├── extends 继承(支持多继承)
│ └── 声明合并(同名自动合并)
│
├── type 独有
│ ├── 联合类型 A | B
│ ├── 元组类型 [A, B]
│ ├── 映射类型 [K in keyof T]
│ ├── 条件类型 T extends U ? X : Y
│ └── 基本类型别名(type Name = string)
│
├── 冲突表现
│ ├── interface 同名冲突 → 报错提示
│ └── type 交叉冲突 → 静默变 never
│
└── 选择规则
├── 描述对象形状 → interface
├── 需要类型计算 → type
└── 不确定 → 默认 interface
速记口诀
学习成果检查
学完本文后,你应该能够:
[ ] 写出 interface 和 type 的基本语法
[ ] 说出两者在扩展方式上的区别
[ ] 解释什么是声明合并以及为什么只有 interface 能做
[ ] 列举 type 独有的四种能力
[ ] 解释同名冲突时两者的不同表现
[ ] 根据三条规则判断该用 interface 还是 type
推荐资源
下一步
本文是 TypeScript 深入 系列的第 2 天。接下来的学习路线:
第 3 天:联合类型与交叉类型 — 类型组合的艺术
第 4 天:类型守卫与类型断言 — 安全的类型收窄
第 5 天:枚举与字面量类型 — 定海神针
第 6 天:keyof / typeof / 索引访问 — 类型钥匙
第 7 天:第一关 BOSS 战 — 综合实战
学编程就像蜗牛往上爬,慢一点没关系,关键是不停下来。
每天花 2 小时,28 天通关 TypeScript 深入。加油!
本文是「Web 前端工业可视化学习」路线的 TypeScript 阶段第 2 天内容。更多学习内容请关注专栏更新。