前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版

TypeScript 中 interface 与 type 的区别和选型建议

首页2019-09-28 16:25:24Front-End
JavaScriptTypeScript类型系统

interface 和 type 到底该用哪个,是 TypeScript 项目里最容易起争执的问题之一。网上大部分回答停在「差不多,看团队规范」,但真到了写 Record<string, unknown> 报错、两个类型交叉之后属性静默变成 never、或者要给第三方库补类型这几个场景,选错了是要返工的。

这篇把两者真正有差别的地方一条条列出来,每条都在 TypeScript 5.9.3 上验证过,最后给一份可以直接照着执行的选型规则。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 两者在描述对象和函数上的写法差异,以及为什么可以互相 extends
  • type 独有的能力,联合类型、元组、映射类型、条件类型、模板字面量类型
  • interface 独有的声明合并,以及它在给第三方库补类型时的不可替代性
  • extends 和交叉类型在遇到属性冲突时的行为差异,这条最容易踩
  • 隐式索引签名,为什么 interface 赋值给 Record 会报错而 type 不会
  • 报错信息、编译性能,以及最终的选型建议

# 一、相同之处

# 1.1 都可以描述一个对象或者函数

interface

interface User {
  name: string
  age: number
}

interface SetUser {
  (name: string, age: number): void;
}
@前端进阶之旅: 代码已经复制到剪贴板

type

type User = {
  name: string
  age: number
};

type SetUser = (name: string, age: number)=> void;
@前端进阶之旅: 代码已经复制到剪贴板

这里补一个原文没提的细节。上面 SetUser 的两种写法虽然等价,但 interface 那种「调用签名」的形式支持给函数类型追加静态属性,type 要做到同样的事得写交叉类型。日常业务里用不到,写库的时候偶尔会遇到。

# 1.2 都允许拓展(extends)

interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type,type 也可以 extends interface。虽然效果差不多,但是两者语法不同。

interface extends interface

interface Name {
  name: string;
}
interface User extends Name {
  age: number;
}
@前端进阶之旅: 代码已经复制到剪贴板

type extends type

type Name = {
  name: string;
}
type User = Name & { age: number  };
@前端进阶之旅: 代码已经复制到剪贴板

interface extends type

type Name = {
  name: string;
}
interface User extends Name {
  age: number;
}
@前端进阶之旅: 代码已经复制到剪贴板

type extends interface

interface Name {
  name: string;
}
type User = Name & {
  age: number;
}
@前端进阶之旅: 代码已经复制到剪贴板

四种组合都合法,写法上一个用 extends 一个用 &。这里说「效果差不多」是有前提的,前提是两边没有同名属性冲突。冲突的时候差别巨大,第四节专门讲这个。

另外注意 interface extends 的右边只能是对象类型或者由对象类型组成的交叉类型,你没法 interface Foo extends (string | number),因为联合类型不是对象类型。

# 二、type 可以而 interface 不行

type 可以声明基本类型别名、联合类型、元组等类型:

// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong(): void;
}
interface Cat {
    miao(): void;
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
@前端进阶之旅: 代码已经复制到剪贴板

原文这里的 wong(); 和 miao(); 没写返回类型,在 strict 模式下会直接报 TS7010: 'wong', which lacks return-type annotation, implicitly has an 'any' return type。我在 TypeScript 5.9.3 上确认过,所以上面补上了 : void。老文里这类省略很常见,因为当年很多项目不开 strict。

type 语句中还可以使用 typeof 获取实例的类型进行赋值:

// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div
@前端进阶之旅: 代码已经复制到剪贴板

其他骚操作:

type StringOrNumber = string | number;
type Text = string | { text: string };
type NameLookup = Dictionary<string, Person>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>;
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
@前端进阶之旅: 代码已经复制到剪贴板

# 2.1 真正拉开差距的是类型运算

上面这些还只是「别名」层面的能力。type 和 interface 真正的分水岭在于,TypeScript 后来加进来的类型运算能力全部只挂在 type 上。

映射类型,遍历一个类型的所有 key 生成新类型:

type Readonly<T> = { readonly [K in keyof T]: T[K] }
type Partial<T> = { [K in keyof T]?: T[K] }
@前端进阶之旅: 代码已经复制到剪贴板

条件类型,根据类型关系分支:

type NonNullable<T> = T extends null | undefined ? never : T
type ElementOf<T> = T extends (infer U)[] ? U : never
@前端进阶之旅: 代码已经复制到剪贴板

模板字面量类型,用字符串拼接构造类型:

type EventName<T extends string> = `on${Capitalize<T>}`
type Click = EventName<'click'>   // 'onClick'
@前端进阶之旅: 代码已经复制到剪贴板

内置的工具类型 Partial、Pick、Omit、Record、ReturnType 全部是用 type 实现的,因为它们的实现依赖映射类型和条件类型。你想自己写一个通用的类型工具,只有 type 这一条路。

所以「两者差不多」这个说法在 2019 年勉强成立,放到今天已经不成立了。

# 三、interface 可以而 type 不行

interface 能够声明合并:

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string
}
*/
@前端进阶之旅: 代码已经复制到剪贴板

同名的 type 会直接报 TS2300: Duplicate identifier,我在 5.9.3 上跑过,两行都会标红。

# 3.1 声明合并不是玩具特性

第一次看到声明合并,我的反应是这玩意儿听着像个陷阱,同名的东西自动合并,出问题很难查。后来才发现它在一个场景里完全不可替代:给别人的类型补东西。

给全局对象加字段:

declare global {
  interface Window {
    __APP_VERSION__: string
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

给 Express 的 Request 挂上自己的用户信息:

declare module 'express-serve-static-core' {
  interface Request {
    user?: { id: string; role: string }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这两种写法都依赖声明合并,用 type 做不到。你不可能去改 @types/express 的源码,也不可能重新声明一个同名 type。所以只要你的项目需要扩展第三方库或者全局类型,interface 就是必选项。

反过来说,声明合并在业务类型上确实是个隐患。团队里两个人各自在不同文件写了 interface Config,TypeScript 不会报错,只会安静地合并,最后拿到的是一个谁也没预期的类型。这是很多团队规范里写「业务类型统一用 type」的理由。

# 四、extends 和交叉类型,冲突时行为完全不同

这一节是整篇最实用的部分。

同名属性类型冲突时,interface extends 会立刻报错:

interface A { x: string }
interface B extends A { x: number }
// TS2430: Interface 'B' incorrectly extends interface 'A'.
//   Types of property 'x' are incompatible.
//     Type 'number' is not assignable to type 'string'.
@前端进阶之旅: 代码已经复制到剪贴板

交叉类型不报错,它会把两个类型求交集:

type C = { x: string } & { x: number }
declare const c: C
const cx: never = c.x   // 编译通过,c.x 的类型就是 never
@前端进阶之旅: 代码已经复制到剪贴板

string 和 number 没有交集,所以 c.x 静默变成了 never。你把 C 传给别的函数,报错会出现在很远的地方,提示大概是「never 类型的参数不能赋给 string」,然后你要顺着调用链一路往回找,才能找到这个交叉类型。

这个我在 TypeScript 5.9.3 上验证过,两段的行为完全如上。

先说结论,继承关系用 interface extends 比用 & 安全,因为错误会在定义处就暴露,而不是延后到使用处。

# 4.1 隐式索引签名

这条也很容易撞上。同样是描述一个只有 name 的对象,type 声明的能赋值给 Record<string, unknown>,interface 声明的不能:

fe
  • 一、相同之处
    • 1.1 都可以描述一个对象或者函数
    • 1.2 都允许拓展(extends)
  • 二、type 可以而 interface 不行
    • 2.1 真正拉开差距的是类型运算
  • 三、interface 可以而 type 不行
    • 3.1 声明合并不是玩具特性
  • 四、extends 和交叉类型,冲突时行为完全不同
    • 4.1 隐式索引签名
    • 4.2 报错信息和编译性能
  • 五、怎么选
  • 总结
  • 参考

← 本地构建部署easy-mock服务完整流程React Native真机调试完整流程与连不上的排查顺序 →