day02-interface-vs-type

作者:mario 发布时间: 2026-08-03 阅读量:6 评论数:0

TypeScript 接口 vs 类型别名 — 一次讲透,到底用哪个?

在 TypeScript 中定义对象类型,你有两种选择:interfacetype。它们长得像、功能也像,到底该用哪个?本文从语法、扩展、合并、独有能力四个维度,彻底讲透两者的区别与选择策略。


目录


一、两种方式,一个目标

在 TypeScript 中,当你需要描述一个对象的形状时,有两种语法可用:

// 方式一:interface
interface User {
  name: string;
  age: number;
}

// 方式二:type alias(类型别名)
type User = {
  name: string;
  age: number;
};

两者都能描述"一个对象有 name 属性(string 类型)和 age 属性(number 类型)"。

那为什么要有两种?它们的区别到底在哪? 这是 TypeScript 初学者最常见的问题之一。本文从以下维度彻底讲透:

对比维度

interface

type

基本语法

interface Name { ... }

type Name = { ... }

扩展方式

extends

& 交叉类型

声明合并

✅ 支持

❌ 不支持

联合类型

❌ 不支持

✅ 支持

元组类型

❌ 不支持

✅ 支持

映射类型

❌ 不支持

✅ 支持

条件类型

❌ 不支持

✅ 支持


二、基本语法对比

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 基础用法对比表

能力

interface

type

描述对象形状

可选属性 ?

只读属性 readonly

索引签名 [key: T]

描述函数类型

✅ call signature

✅ 箭头函数(更简洁)

联合类型 A | B

元组类型 [A, B]

基本类型别名


四、扩展方式对比

当一个类型需要基于另一个类型进行扩展时,两者的写法不同。

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

type

语法

extends

& 交叉类型

多继承

extends A, B

A & B & C

可读性

语义更直观

语法更紧凑

冲突处理

报错提示

静默变为 never(见第七节)


五、声明合并 — 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 行为

type 行为

同名非冲突属性

✅ 自动合并

❌ 报错 Duplicate identifier

同名冲突属性

❌ 报错提示冲突

❌ 报错 Duplicate identifier

扩展第三方库

✅ 用 declare module

❌ 无法扩展

记住:声明合并是 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 四大能力总结

能力

语法

示例

interface 能做?

联合类型

A | B

type Status = "on" | "off"

元组类型

[A, B]

type Point = [number, number]

映射类型

[K in keyof T]

type Readonly<T> = ...

条件类型

T extends U ? X : Y

type IsString<T> = ...

结论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

type

报错方式

编译报错,明确提示

静默变成 never

发现难度

低(立刻看到)

高(运行时才发现)

安全性


八、到底怎么选?三条决策规则

不要纠结,记住这三条规则就够了:

规则一:描述对象形状 → 用 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 是公式 — 定义后不能改,但可以组合、变形、嵌套。

详细类比

interface(合同)

type(公式)

定义后能否追加

✅ 可以(声明合并)

❌ 不能(同名报错)

能否有矛盾条款

❌ 不能(报错提示)

❌ 不能(变 never)

能否组合

✅ extends

✅ &

能否做计算

❌ 不能

✅ 联合/映射/条件

核心定位

描述"形状"

描述"计算"


十、常见坑点与最佳实践

坑点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!

最佳实践清单

  1. 默认用 interface 描述对象形状:更直观、报错更友好

  2. 需要联合/元组/映射/条件时用 type:这些是 type 的专属能力

  3. 扩展第三方库类型用 interface + declare module:声明合并是唯一方式

  4. 避免在 type 中制造隐式 never:同名冲突用 interface 暴露问题

  5. 不要混用两种风格:同一个项目内保持一致

  6. 函数类型用 type:箭头函数语法更简洁

  7. 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 特权

extends 继承 + 声明合并

type 特权

联合 + 元组 + 映射 + 条件

冲突表现

interface 报错,type 变 never

选择策略

描述形状用 interface,类型计算用 type

一句话类比

interface 是合同,type 是公式

学习成果检查

学完本文后,你应该能够:

  • [ ] 写出 interface 和 type 的基本语法

  • [ ] 说出两者在扩展方式上的区别

  • [ ] 解释什么是声明合并以及为什么只有 interface 能做

  • [ ] 列举 type 独有的四种能力

  • [ ] 解释同名冲突时两者的不同表现

  • [ ] 根据三条规则判断该用 interface 还是 type


推荐资源

资源

说明

TypeScript Handbook - Interfaces

官方 interface 文档

TypeScript Handbook - Type Aliases

官方 type alias 文档

TypeScript Handbook - Everyday Types

日常类型用法

TypeScript Playground

在线实验

type-challenges

类型体操刷题

TypeHero

闯关式学习


下一步

本文是 TypeScript 深入 系列的第 2 天。接下来的学习路线:

  • 第 3 天:联合类型与交叉类型 — 类型组合的艺术

  • 第 4 天:类型守卫与类型断言 — 安全的类型收窄

  • 第 5 天:枚举与字面量类型 — 定海神针

  • 第 6 天:keyof / typeof / 索引访问 — 类型钥匙

  • 第 7 天:第一关 BOSS 战 — 综合实战


学编程就像蜗牛往上爬,慢一点没关系,关键是不停下来。

每天花 2 小时,28 天通关 TypeScript 深入。加油!


本文是「Web 前端工业可视化学习」路线的 TypeScript 阶段第 2 天内容。更多学习内容请关注专栏更新。

评论