TypeScript面试题:interface 与 type:相同点、核心区别与选择指南
- 前言
- [1. 基础共性:都能描述对象结构](#1. 基础共性:都能描述对象结构)
-
- [1.1 用两种语法约束同一个对象](#1.1 用两种语法约束同一个对象)
- [1.2 都能在已有对象类型上继续扩展](#1.2 都能在已有对象类型上继续扩展)
- [2. 核心区别一:interface 支持声明合并](#2. 核心区别一:interface 支持声明合并)
- [3. 核心区别二:type 能直接表示联合类型和元组](#3. 核心区别二:type 能直接表示联合类型和元组)
- [4. 再看共性:都能描述函数签名](#4. 再看共性:都能描述函数签名)
- [5. 实战选择与面试表达](#5. 实战选择与面试表达)
-
- [5.1 一张表梳理相同点与不同点](#5.1 一张表梳理相同点与不同点)
- [5.2 选择时不要陷入二选一](#5.2 选择时不要陷入二选一)
- 总结
前言
在 TypeScript 中,interface 和 type 都能描述数据的类型。刚接触它们时,最容易产生的疑问是:既然两者都能约束对象,为什么还要设计两套语法?实际开发中又该如何选择?
这两个关键字的能力确实存在较大的重叠,但设计侧重点并不相同。interface 更偏向描述可扩展的对象契约,type 更像是为任意类型表达式起一个名字。 理解这一点之后,对象结构、继承、声明合并、联合类型、元组和函数签名之间的关系就会变得清晰。
学习
interface与type,重点不是死记"谁能做什么",而是先看它们共同解决了什么问题,再理解各自不可替代的能力。
1. 基础共性:都能描述对象结构
1.1 用两种语法约束同一个对象
TypeScript 的静态类型检查发生在编译阶段。为对象声明类型,本质上是在告诉编译器:这个值应该有哪些属性,每个属性又应该保存什么类型的数据。
typescript
interface User {
name: string;
age: number;
avatarUrl: string;
}
type UserType = {
name: string;
age: number;
avatarUrl: string;
};
const u1: User = {
name: 'moss',
age: 18,
avatarUrl: '/avatar.png'
};
const u2: UserType = {
name: 'moss',
age: 18,
avatarUrl: '/avatar.png'
};
编译器检查 u1 和 u2 时,关注的是对象的结构 :name 必须是字符串,age 必须是数字,avatarUrl 也必须是字符串。由于两个对象都满足对应约束,因此检查可以通过。
这里体现了 TypeScript 的结构化类型系统 。类型是否兼容,主要取决于成员结构,而不是类型名称。换句话说,User 和 UserType 虽然名字不同、声明方式不同,但它们描述的结构完全一致,因此在多数对象类型场景中可以发挥相同作用。
| 对比项 | interface User |
type UserType |
|---|---|---|
| 描述对象属性 | 支持 | 支持 |
| 检查属性类型 | 支持 | 支持 |
| 约束缺失的必填属性 | 支持 | 支持 |
| 参与运行时逻辑 | 不参与 | 不参与 |
需要注意的是,类型声明只服务于编译阶段。以上代码没有 console.log,所以运行后不会打印内容;经过 TypeScript 编译后,interface 和 type 相关声明都会被移除,真正留在 JavaScript 中的是 u1、u2 这些运行时变量。
1.2 都能在已有对象类型上继续扩展
实际业务中的类型往往不是从零开始设计。例如员工首先是一个人,因此员工应该拥有人的姓名,同时还需要自己的岗位信息。interface 使用 extends 表达这种扩展关系,type 则通过交叉类型 & 合并多个结构。
typescript
interface Person {
name: string;
}
interface Employee extends Person {
job: string;
}
type PersonType = {
name: string;
};
type EmployeeType = PersonType & {
job: string;
};
const e1: Employee = {
name: 'moss',
job: 'Agent 开发工程师'
};
const e2: EmployeeType = {
name: 'moss2',
job: 'C++ 开发工程师'
};
编译器处理 Employee 时,会先读取 Person 中的 name,再加入自身的 job;处理 EmployeeType 时,则会计算 PersonType 与 { job: string } 的交叉结果。最终得到的有效结构都是 { name: string; job: string },所以 e1 与 e2 都必须同时提供 name 和 job。
两种写法表达了相近的目标,但语义角度略有差别:
extends强调"在某个对象契约上继续扩展",阅读起来更接近继承关系。&强调"多个类型必须同时满足",适合组合已有类型。- 如果交叉的同名属性互相冲突,结果可能得到难以赋值的
never,因此不能把&简单理解为对象属性的无条件拼接。
2. 核心区别一:interface 支持声明合并
interface 最有辨识度的能力之一是声明合并。同一作用域内多次声明同名接口时,TypeScript 会把这些声明收集起来,并合并为一个完整的对象契约。
typescript
interface Animal {
name: string;
}
interface Animal {
age: number;
}
const dog: Animal = {
name: 'moss',
age: 18
};
这段代码的检查过程可以理解为:编译器第一次看到 Animal 时记录 name,第二次看到同名接口时继续加入 age,最终 Animal 同时要求这两个属性。因此,dog 只写 name 或只写 age 都无法通过检查。
声明合并不是后一次声明覆盖前一次声明,而是多个同名
interface共同组成最终契约。
type 不具备这项能力,同一作用域中重复声明同名类型别名会直接产生"标识符重复"错误。
typescript
type AnimalType = {
name: string;
};
// type AnimalType = {
// age: number;
// }; // 错误:同名类型别名不能重复声明
声明合并适合需要开放扩展的类型设计,例如第三方库对全局对象的类型补充。不过在普通业务模型中,也要避免把同一个接口拆得过于分散,否则阅读者很难在一个位置看清完整结构。
| 场景 | interface |
type |
|---|---|---|
| 同名声明出现多次 | 自动合并 | 编译报错 |
| 适合开放式扩展 | 更适合 | 不适合通过同名声明扩展 |
| 保持类型定义位置集中 | 需要团队主动约束 | 天然要求名称唯一 |
3. 核心区别二:type 能直接表示联合类型和元组
如果需求不再是"描述一个对象有哪些成员",而是"一个值可能属于哪些类型",type 的表达能力会更自然。它可以直接为联合类型、元组以及其他类型表达式命名。
typescript
type ID = string | number;
type Point = [number, number];
const userId: ID = 'moss-001';
const center: Point = [120, 30];
ID 表示一个值可以是 string,也可以是 number。编译器检查 userId 时,只要它满足联合成员中的任意一种即可。Point 则表示一个固定结构的数组:长度为两个位置,并且两个位置都必须是数字。center 恰好满足这一约束。
interface 的语法核心是声明对象成员,不能使用下面这种方式直接把接口赋值为联合类型:
typescript
// interface ID = string | number; // 错误:interface 不能这样表示联合类型
即使改成合法的接口声明语法,也不能让它与前面的类型别名共用 ID 这个名称:
typescript
type ID = string | number;
interface ID {} // 错误:标识符 ID 重复
这里要区分两个报错原因:带等号的写法不符合 interface 的语法;空接口的写法本身合法,但不能与同名的 type 声明合并。接口的声明合并只发生在多个同名 interface 之间。
这背后不是简单的语法差异,而是两种工具的设计重心不同:interface 声明的是可扩展的对象契约,type 声明的是某个类型表达式的别名。联合类型 string | number 和元组 [number, number] 都属于完整的类型表达式,所以用 type 命名最直接。
| 类型需求 | interface |
type |
|---|---|---|
| 对象结构 | 支持 | 支持 |
| 联合类型 | 不能直接表示 | 支持 |
| 元组类型 | 不适合直接表示 | 支持 |
| 基础类型别名 | 不支持 | 支持 |
4. 再看共性:都能描述函数签名
函数也是有结构的:需要接收哪些参数,以及最终返回什么结果。interface 可以使用调用签名描述函数,type 则可以直接为函数类型表达式起别名。
typescript
interface AddFn {
(a: number, b: number): number;
}
const add1: AddFn = (x, y) => x + y;
type AddType = (a: number, b: number) => number;
const add2: AddType = (x, y) => x + y;
赋值发生时,AddFn 和 AddType 都会为箭头函数提供上下文类型,因此参数 x、y 会被推断为 number,返回值也必须是 number。如果函数返回字符串,编译器就会指出返回类型不符合约束。
这段代码同样不会主动打印结果。若分别调用 add1(1, 2) 和 add2(1, 2),两者都会返回数字 3。这说明调用方式和运行结果没有区别,区别只存在于类型声明的写法与后续扩展能力上。
对单纯的函数类型而言,
type写法通常更紧凑;当函数对象还需要附加属性,或希望利用接口扩展与声明合并时,interface会更有表达力。
5. 实战选择与面试表达
5.1 一张表梳理相同点与不同点
经过前面的四组场景,可以把核心结论压缩为下面这张表:
| 能力 | interface |
type |
结论 |
|---|---|---|---|
| 描述对象结构 | 支持 | 支持 | 两者最常见的重叠能力 |
| 复用对象类型 | 使用 extends |
使用交叉类型 & |
都能扩展,语义侧重不同 |
| 声明合并 | 支持 | 不支持 | 开放扩展优先考虑 interface |
| 联合类型 | 不能直接表示 | 支持 | 此场景使用 type |
| 元组类型 | 不适合直接表示 | 支持 | 此场景使用 type |
| 函数签名 | 使用调用签名 | 使用函数类型表达式 | 两者都能完成约束 |
| 编译后是否保留 | 不保留 | 不保留 | 都只参与静态类型检查 |
5.2 选择时不要陷入二选一
实际开发不需要规定整个项目只能使用其中一种。更稳妥的判断方式是先看要表达的类型本质:
- 主要描述对象、类的公共契约,并且希望后续可以扩展或合并时,优先考虑
interface。 - 需要联合类型、元组、基础类型别名,或需要组合出更灵活的类型表达式时,使用
type。 - 只是描述一个稳定、封闭的普通对象结构时,两者都可以,遵循团队已有规范比争论语法更重要。
- 使用交叉类型时留意同名属性冲突;使用声明合并时控制声明位置,避免类型来源过于分散。
面试中可以先给出一句总判断,再补充例子:interface 和 type 都能描述对象与函数,也都能复用已有对象类型;主要差异是 interface 支持声明合并,而 type 能直接表示联合类型、元组等更广泛的类型表达式。 最后说明选择原则,就能形成完整回答,而不是零散地罗列语法。
总结
interface 与 type 并不是互相替代的竞争关系,而是 TypeScript 类型系统中侧重点不同的两种工具。对于对象结构,两者都可以完成属性检查;对于类型复用,interface 使用 extends,type 使用交叉类型 &;对于函数签名,两者也都能提供参数与返回值约束。真正拉开差异的是开放性和表达范围:interface 支持声明合并,更适合可扩展的对象契约;type 可以命名联合类型、元组和其他类型表达式,适合灵活组合。掌握这些边界之后,选择就会从"记规则"变成"看需求"。