想象你开了一家餐厅,后厨要同时管理两套“描述系统”。一套是《员工手册》(interface):每个岗位都有一张固定表格,写着姓名、工号、入职日期,表格必须规规范范,因为它是用来约束人和流程的。另一套是贴在灶台上的《配方卡》(type):它更像是便利贴——“招牌酱汁 = 生抽 + 蜂蜜”,可以极其简短,口味还能千变万化。TypeScript(简称 TS,一种给 JavaScript 加类型检查的语言)里 interface 与 type 的关系,就像员工手册与配方卡。它们都能描述“一样东西长什么样”,但手册的规矩是“补充、合并、继承”,配方卡的看家本领是“混合、计算、定制”。很多开发者纠结于“interface vs type 怎么选”,其实只需要记住:先分清你要描述的是“一份岗位表”,还是一张“可灵活调配的配方”。

岗位关联

对 AI 从业者的价值:AI 工程里无论写 Agent 工具函数、RAG 检索器还是模型服务的入参出参,都要用 TS 定义类型。选择谁更合适,直接决定库的可扩展性和团队协作效率——面试常考,写代码天天用。

目录
  1. 可视化演示
  2. 基本语法差异
  3. 声明合并特性
  4. type独有能力
  5. 扩展方式区别
  6. 索引签名场景
  7. 性能与产物
  8. 社区最佳实践
  9. 选择建议总结
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

双栏对比展示TypeScript中interface与type的核心差异:语法能力、声明合并、扩展方式和最佳实践,切换维度查看每个方面的支持情况和适用场景。

本节配有交互动效(进入视口后自动加载,约 3KB)。

语法差异对比 interface擅长描述对象形状和类结构,支持extends扩展 type支持联合、交叉、条件、元组、映射等高级类型运算 type = interface ∪ {union, intersection, conditional, tuple, mapped} 语法:type能力范围更广

基本语法差异

餐厅的《员工手册》里,每个岗位都有一张固定表格:姓名栏、工号栏、入职日期栏。表格必须规规范范,因为“收银员”这个岗位长什么样,由表格说了算。而灶台边的《配方卡》只是便利贴:写着“招牌酱汁 = 生抽 + 蜂蜜”,它本身可以是任何东西的简短代号。interface 与 type 的第一道分水岭,就在这“表格”与“便利贴”之间。

在 TypeScript 里,interface 用一对花括号描述“对象应该长成什么样”,语法就像填表:``interface User { name: string; age: number }``。而 type 用等号,右边不一定是对象,可以是字符串、数字、数组、函数签名等任何已有类型。换句话说,interface 天生是“对象的模具”,type 天生是“任意类型的别名”。

两者的思维导向因此不同。如果你写 ``type UserID = string | number``,一眼就知道 UserID 可能是字符串也可能是数字,这种“或”的表达 interface 做不到;反过来,如果只是想表达“一个 User 对象有 name 和 age 两个字段”,interface 的表格感更直白。很多团队据此定下第一条规则:描述对象形状优先 interface,描述“一个任意类型的快速叫法”用 type。

所以,第一步不是比谁更强大,而是看素材像什么:要画一张岗位表就翻开员工手册,要写一张便签就抽出配方卡。先选对“描述工具”的类型,后面的烦恼会少一半。

interface  User={name:string,  age:number}type  UserID=string∣number\texttt{interface}\;User = \{ name: string,\; age: number \}\qquad\texttt{type}\;UserID = string \mid number
上面展示了两种声明:interface 用“模板”框住一个对象结构,type 用“等号”给任意类型起一个名字。
  1. interface 关键字声明“这是一张图纸,用来规定对象长什么样”。
    interface  User={name:string,  age:number}\texttt{interface}\;User = \{ name: string,\; age: number \}
  2. 花括号内容里面的每一项都是“属性名:类型”,表示 User 必须有 name 和 age 两个字段。
    {name:string,  age:number}\{ name: string,\; age: number \}
  3. type 关键字与竖线type 用 = 绑定右边的类型表达式,| 表示“或”,所以 UserID 要么是 string 要么是 number。
    type  UserID=string∣number\texttt{type}\;UserID = string \mid number

声明合并特性

《员工手册》是活页本。上午在“收银员”表格里补一行“需会扫码”,下午又补一行“需会开发票”,最终定稿时,所有补充项会自动汇成一份完整的岗位说明,谁也不用覆盖谁。但《配方卡》是印刷卡片:如果你印了两张同样叫“招牌酱汁”的卡片,后厨只会以为你印错了——两张卡没法自动并成一张。

这正是 interface 与 type 最令人印象深刻的分歧点。在 TS 中,同名 interface 可以出现多次,编译器会把它们的所有属性合并成一个完整接口,这叫声明合并(declaration merging)。而同名 type 一旦重复声明,直接报"Duplicate identifier"(重复标识符)错误,绝不会悄悄合并。

这个特性最实用的场景是“补充别人定义好的类型”。比如浏览器全局对象 Window 由官方声明,但你想给它挂一个自己的全局变量,就可以在自己的 .d.ts 文件里再写一个 ``interface Window { myGlobal: string }`,TS 会自动把 myGlobal` 并进官方 Window 类型,像在别人的手册上合法加页。

活页本的价值在于“持续补充而不破坏原页”。interface 的声明合并让团队可以安全地给第三方类型追加字段,而 type 的“一锤定音”则保证了配方卡的纯净——这是两种完全不同的取舍哲学。

interface  Box(1)  ⊕  interface  Box(2)  ⟹  interface  Boxmerged\texttt{interface}\;Box^{(1)} \;\oplus\; \texttt{interface}\;Box^{(2)} \;\Longrightarrow\; \texttt{interface}\;Box_{merged}
同一个名字的 interface 可以多次声明,用“加法”合并成一份。
  1. Box 出现两次同名 interface 允许重复书写,这是 type 做不到的。
    interface  Box(1)interface  Box(2)\texttt{interface}\;Box^{(1)} \qquad \texttt{interface}\;Box^{(2)}
  2. 加法符号 ⊕表示把两次声明的所有属性取并集合并。
    interface  Box(1)  ⊕  interface  Box(2)\texttt{interface}\;Box^{(1)} \;\oplus\; \texttt{interface}\;Box^{(2)}
  3. 箭头结论合并后得到一个包含全部属性的 Box,编译时自动完成。
    ⟹  interface  Boxmerged\Longrightarrow\; \texttt{interface}\;Box_{merged}

type独有能力

如果说 interface 是一本装订好的手册,那 type 就是一张可以任意调配的配方卡。后厨的利器在于“混合”:把鸡尾酒配方写成“威士忌或朗姆”,这是联合类型(要么 A 要么 B);把甜品配方写成“既要是蛋糕又要能冷藏”,这是交叉类型(A 和 B 都要);还有“如果客人能吃辣就上麻婆豆腐,否则上清蒸鱼”的条件类型,以及“把一张菜单批量改成英文版”的映射类型。这些配方级操作,员工手册一概做不到。

在 TS 里,这些能力全部由 type 扛起。联合类型写 string | number,交叉类型写 A & B,条件类型写 T extends U ? X : Y,映射类型写 [K in keyof T]。interface 的语法结构是“属性名:类型”的列表,天生无法表达“这个对象要么整个人出现,要么只出现一部分”这种灵活性。元组(Tuple,固定长度数组)如 [string, number] 也必须借助 type。

正因为 type 支持这些“类型计算”,它成了写工具类型(Utility Types)的舞台。比如 TS 内置的 Partial<T>(把 T 全部属性变成可选)就是映射类型的经典产物。你可以用 interface 描述“一个已知对象长得怎样”,但描述“一个对象经过某种变换之后长什么样”,只能靠 type。

配方卡的灵魂是“变化”。当你面对的数据形态多变——例如大模型的输出“要么是文本,要么是结构化 JSON,要么是工具调用”——type 的联合与条件能力就是唯一顺手的工具。记住:想描述“一个固定的对象”,找手册;想做“类型之间的运算”,找配方。

T=A∪BU=A∩BC=T  extends  U  ?  X:YT = A \cup B \qquad U = A \cap B \qquad C = T \;\texttt{extends}\; U \;?\; X : Y
联合、交叉、条件,这三板斧是 type 的独门绝活。
  1. 集合符号 ∪表示 T 是“A 或 B”,这就是联合类型。
    T=A∪BT = A \cup B
  2. 集合符号 ∩表示 U 必须同时具备 A 与 B 的全部属性,这就是交叉类型。
    U=A∩BU = A \cap B
  3. 条件表达式如果 T 能赋值给 U,结果取 X,否则取 Y,相当于类型的"if-else"。
    C=T  extends  U  ?  X:YC = T \;\texttt{extends}\; U \;?\; X : Y

扩展方式区别

员工手册的扩展方式是“继承”:《主管手册》 extends《员工手册》,意思是主管自动拥有员工的一切职责,再额外加上管理条款,这是一种自上而下的等级关系。配方卡的扩展方式是“混合”:把红烧肉配方和土豆配方混在一起,得到“土豆烧肉”,两种材料平起平坐,谁也不是谁的上级。

在 TS 中,interface 用 extends 关键字继承另一个 interface 或 class,继承后自动获得父级全部属性,再追加新属性。type 没有 extends 语法,它用交叉类型 A & B 把多个类型组合成一个整体。这两种扩展的哲学完全不同:extends 强调“从哪来、上溯到谁”,& 强调“由哪些部分拼装而成”。

这里有一个经典陷阱:interface 能用 extends 去扩展一个 type,但前提是这个 type 必须是一个对象类型,比如 ``type Animal = { name: string }`。一旦 type 是联合类型(如 string | number),interface extends 会直接报错,因为手册无法继承一张"要么威士忌要么朗姆"的配方卡。反过来说,type 却可以放心地用交叉类型组合一切:`type Mixed = User & Admin`` 毫无压力。

扩展方式的选择,本质上是选择“层级制度”还是“拼装文化”。面向对象风格的项目里,员工手册(interface)的继承关系清晰可查;函数式或数据密集型代码里,配方卡(type)的交叉组合更自由。如果你发现一个类型需要“继承一个联合类型”,那答案只有一个:放下 interface,拿起 type。

interface  Child  extends  Parenttype  Mixed=A  &  B\texttt{interface}\;Child \;\texttt{extends}\; Parent\qquad\texttt{type}\;Mixed = A \;\&\; B
interface 是“纵向继承”,type 是“横向拼装”。
  1. extends 关键字Child 继承 Parent 的全部属性,并允许继续添加新字段。
    interface  Child  extends  Parent\texttt{interface}\;Child \;\texttt{extends}\; Parent
  2. 与符号 &把 A 和 B 的所有属性合并成 Mixed,逻辑上等价于“同时满足 A 与 B”。
    type  Mixed=A  &  B\texttt{type}\;Mixed = A \;\&\; B
  3. 限定关系interface extends 只能接对象类型的 type,不能接联合类型。
    interface  Child  extends  (string∣number)⟶错误\texttt{interface}\;Child \;\texttt{extends}\; (string \mid number) \longrightarrow \text{错误}

索引签名场景

《员工手册》最后一页是“附录索引表”:登记着员工的编号规则——“编号:字符串,值:员工姓名”。有了这张索引表,任何符合规则的员工记录都能被快速查找。配方卡也有类似设计:一个“任意调料格”,写着 [调料名: string]: 用量,方便后厨动态补充新调料,而不必为每种调料单独写一张卡。

在 TS 中,interface 和 type 都能定义索引签名(Index Signature),写法类似 [key: string]: unknown,表示“这个对象的键是字符串,值是 unknown 类型”。但两者的差异藏在细节里:interface 如果多次声明同一个名字,每次声明的索引签名类型必须保持兼容一致,否则合并时直接报错;type 因为没有声明合并,自然没有这个烦恼。

还有一个容易被忽视的规则:interface 里一旦定义了索引签名 [key: string]: string,那么它内部的普通属性(如 age: number)就必须兼容 string 类型,否则报错——因为 age 也会被当成索引的一部分。type 虽然也遵守这个规则,但它的优势在于没有“重复声明合并”的额外约束,组合多个带索引签名的类型时更加自由。

索引签名的场景在 AI 工程里非常常见:当你把一整个配置文件或模型返回的字典对象接进来时,往往希望“键值自由但类型统一”。遇到这种“附录索引表”式的需求,两者都能做;但如果涉及多个带索引签名的类型要合并,type 的自由度更高,而 interface 则更适合“固定一张表、不许乱合并”的场景。

interface  Dict={[key:string]:unknown}type  Dict={[K∈string]:V}\texttt{interface}\;Dict = \{ [key: string]: unknown \}\qquad\texttt{type}\;Dict = \{ [K \in string]: V \}
两种声明都能定义“任意键查值”的字典结构,但合并规则不同。
  1. 方括号 key[key: string] 表示对象的键是字符串,这叫索引签名。
    {[key:string]:unknown}\{ [key: string]: unknown \}
  2. unknown 值值类型为 unknown,即“先不限定,用时再收窄”。
    unknownunknown
  3. K in stringtype 用更接近集合遍历的写法表达同样的意思。
    {[K∈string]:V}\{ [K \in string]: V \}

性能与产物

宴会结束后,员工手册和配方卡都要收进档案柜——它们只是“描述”,不是菜品本身。TS 也一样:编译成 JavaScript 后,interface 和 type 统统消失得干干净净,不留任何运行时代码。你写一万行类型定义,打包出来的 JS 文件里一个都找不见。

既然如此,为什么官方 wiki 还提到"Prefer Interfaces Over Intersections"(优先接口而非交叉类型)?核心原因是类型缓存:interface 的合并结构可以被编译器缓存,多次引用时直接复用;type 的交叉类型(特别是复杂的大交叉)每次使用都可能重新展开、重新计算,在大规模代码库里会拖慢编译速度。这就像手册每次引用都直接翻档案库,而配方卡每次都要重新称量材料。

需要说明的是,这个性能差异对绝大多数项目来说微乎其微,真正让 TS 编译变慢的往往是深层的条件类型和递归类型(这些必须用 type 写)。所以结论是:不用为了性能而恐慌性地避开 type,但要理解“interface 更轻、type 更重”的底层原因,在超大项目中把对象描述尽量交给 interface。

理解了“编译后蒸发”,你就不会再把 interface 和 type 的差异想成“运行时性能之争”。“类型是编译期的私事”这个认知,是写出高质量 TS 代码的前提。

TS  →  compile    JS  ∖  {type information}\mathbb{TS}\;\xrightarrow{\;compile\;}\;\mathbb{JS}\;\setminus\;\{\texttt{type information}\}
类型信息在编译时被完全移除,因此不会影响运行性能。
  1. TS 源码编译前的 TypeScript 文件包含类型标注。
    TS\mathbb{TS}
  2. 编译箭头经过编译器处理,输出 JavaScript。
    →  compile  \xrightarrow{\;compile\;}
  3. 差集符号输出的 JS 中“减去”所有类型信息,interface 和 type 都无影无踪。
    JS  ∖  {type information}\mathbb{JS}\;\setminus\;\{\texttt{type information}\}

社区最佳实践

每个行业都有约定俗成的规矩:中餐后厨里,切墩的用菜刀、片鸭的用片刀,不是说不能用别的刀,而是大家默认这么分工,协作效率最高。TypeScript 社区也形成了这样的“用刀默契”——官方手册明确建议:优先使用 interface,直到你需要 type 的特性。

这条建议的落地非常明显。React 官方类型定义中,组件 props 和 state 几乎全部用 interface 描述,因为它们代表“组件对外承诺的形状”,需要可继承、可合并;而函数重载的类型签名、工具类型内部、联合类型,则大量使用 type。DefinitelyTyped(社区维护的类型仓库)里同样遵循这个惯例:公共 API 的对象结构用 interface,内部计算的复杂类型用 type。

eslint 也助推了这一共识:@typescript-eslint/consistent-type-definitions 规则默认要求“能用 interface 就用 interface,否则用 type”。这意味着在规范的团队里,代码会形成一种整齐的韵律:读一个类型定义,先看到 interface 就知道它是“契约”,看到 type 就知道它是“计算”。混用不是报错,但会破坏这种韵律感,让阅读者每次都要停下来猜。

行业惯例不是法律,但它是无数项目踩坑后的总结。跟着社区走,你的代码会让同事感到熟悉和安全:看到 interface 安心它可扩展,看到 type 知道它是拿来算的。一致的风格本身就是一种可维护性。

Best practice:    ∀  object shapes:  prefer interface\text{Best practice:}\;\; \forall\; \text{object shapes}:\; \texttt{prefer interface}
社区共识:凡是描述“一个固定对象的形状”,一律优先 interface。
  1. 全称量词 ∀对于所有“对象形状”的场景。
    ∀  object shapes\forall\; \text{object shapes}
  2. prefer interface都优先选择 interface,这是官方手册的第一原则。
    prefer interface\texttt{prefer interface}

选择建议总结

给后厨贴一张“点餐决策卡”:要描述岗位→翻员工手册;要调配新菜→抽配方卡;要补充规则→往手册加页;要做特殊混合→拿配方卡做文章。TS 类型选择同样可以浓缩成一张决策卡,让你五秒内做出判断。

具体规则如下:需要描述一个对象形状(比如 AI 函数的入参对象、后端返回的用户数据)→ 用 interface;需要声明合并(给全局对象补字段、设计可扩展的插件 API)→ 用 interface;需要定义联合类型、交叉类型、条件类型、映射类型或元组 → 用 type;需要给基础类型或函数签名起别名 → 用 type;面向公共 API 和开源库 → 对象类型首选 interface,复杂的工具类型用 type 并向外部暴露明确的定义。

如果还是拿不准,记住官方手册的原话:“当你不确定时,用 interface,直到 type 能解决你遇到的问题。” 这句话的精髓不是贬低 type,而是告诉你:interface 是默认选项,type 是精确制导武器。团队协作中,比选哪个更重要的是“统一”,因此把这张决策卡写进项目的 README,让所有人都按同一套逻辑选择,远比争论“某一次该用谁”更有价值。

这张决策卡就是全文的落点。员工手册和配方卡没有谁取代谁,它们服务于不同的厨房场景。你现在需要的不是记住每一条语法细节,而是带着这张决策卡回到你的项目里,随手翻开一个类型定义——问自己:这是表格,还是配方?答案自然浮现。

pick={interfaceobject shape, merge, OOPtypeunion, tuple, conditional, mapped\texttt{pick} = \begin{cases} \texttt{interface} & \text{object shape, merge, OOP} \\ \texttt{type} & \text{union, tuple, conditional, mapped} \end{cases}
选择规则:对象、合并、面向对象继承选 interface;联合、元组、条件、映射选 type。
  1. 分段函数pick 结果由场景决条件决定,两个分支互不重叠。
    pick={⋯\texttt{pick} = \begin{cases} \cdots \end{cases}
  2. 第一个分支如果目标是对象形状、声明合并或面向对象继承,选 interface。
    interface  ∣  object shape, merge, OOP\texttt{interface} \;|\; \text{object shape, merge, OOP}
  3. 第二个分支如果目标是联合、元组、条件或映射类型,选 type。
    type  ∣  union, tuple, conditional, mapped\texttt{type} \;|\; \text{union, tuple, conditional, mapped}

AI 应用场景

给 AI 工具库设计公共 API 类型

阶段:Agent 工具链开发 / SDK 封装

解决的问题:写一个供其他开发者调用的 AI 函数库时,如果入参出参用 type 定义对象,使用者无法通过声明合并追加字段,只能 fork 源码修改;改用 interface 定义参数对象,下游开发者就能在自己的命名空间里安全扩展。

具体案例:你发布了一个 RAG 检索器 SDK,方法签名是 ``interface RetrieverInput { query: string; topK: number }`。某大型团队接入时想追加一个 metadata 字段,他们只需在自己的 .d.ts` 中再次声明同名 interface 并补充字段,无需改动你的源码。

定义 LLM Function Calling 的输入 Schema

阶段:模型服务接入 / 工具参数解析

解决的问题:大模型工具调用(Function Calling)的入参常常是“要么文本、要么图片、要么结构化参数”的多选形态,用 interface 无法表达;type 的联合和元组类型可以精准描述,并自动推导出 JSON Schema。

具体案例:一个智能客服 Agent 的 send_message 工具入参被定义为 ``type MsgInput = { type: 'text'; content: string } | { type: 'image'; url: string } | { type: 'tool_call'; toolId: number }``,LLM 返回的参数经过类型收窄后,代码分支每个字面量类型都能获得自动补全。

在 d.ts 声明文件中混用两者

阶段:前端基础设施 / 类型环境搭建

解决的问题:项目的全局类型既要能被任意文件补充(interface 合并),又要复用好几个复杂工具类型(type 计算)。混用让每种工具待在最适合的位置。

具体案例:在一个 AI 标注平台的前端里,``interface Window { __labEnv: 'prod' | 'staging' }` 负责全局注入,而 type PartialBy<T, K>` 这类工具函数用 type 编写,实现了“契约归契约,算法归算法”的划分。

Python 演示

python42 行
"""模拟 TS 中 interface 与 type 的核心行为差异"""class TypeRegistry:    """一个迷你类型登记处,模拟 TS 编译器的类型收集过程"""    def __init__(self):        self.interface_map = {}   # 名字 -> 合并后的属性字典        self.type_alias_map = {}  # 名字 -> 类型表达式     def declare_interface(self, name, props):        # 模拟 interface 声明合并:同名接口自动合并属性        prev = self.interface_map.get(name, {})        self.interface_map[name] = {**prev, **props}        return dict(self.interface_map[name])     def declare_type(self, name, value):        # 模拟 type 声明:同名 type 直接报错        if name in self.type_alias_map:            raise ValueError("type " + name + " 重复声明")        self.type_alias_map[name] = value     def union(self, *names):        # 模拟 type 的联合类型组合        return [self.type_alias_map[n] for n in names]  def main():    reg = TypeRegistry()    # interface 可以多次声明,自动合并成一份    part1 = reg.declare_interface("User", {"id": "string"})    part2 = reg.declare_interface("User", {"name": "string"})    print("interface 合并结果:", part1, part2)    # type 只能声明一次    reg.declare_type("Status", "string | number")    try:        reg.declare_type("Status", "boolean")    except ValueError as exc:        print("type 重复声明报错:", exc)    # type 擅长组合出联合类型    print("type 联合组合:", reg.union("Status"))  if __name__ == "__main__":    main()
目的:用 Python 还原 TS 中 interface 的声明合并且 type 禁止重复声明的行为,帮助理解两者的本质区别。
  1. self.interface_map = {}用字典存放接口合并结果,键是类型名,值是属性集合。
  2. def declare_interface(self, name, props)对应 TS 的同名 interface 声明行为。
  3. self.interface_map[name] = {**prev, **props}把旧属性和新属性合并成一个新字典,模拟“活页加页”。
  4. if name in self.type_alias_map: raise ValueError(...)同名 type 直接抛异常,模拟“重复标识符”错误。
  5. def union(self, *names)把多个 type 表达式收集进列表,模拟联合类型的“多选一”思想。

常见误区

误区“interface 的语法是 `{}`,那它只能描述普通对象,函数类型、数组、元组这些只能用 type 写。”

正解interface 也能描述函数类型,例如 ``interface Fn { (x: number): string }` 等价于 type Fn = (x: number) => string;它还能通过索引签名描述数组或字典。只不过语法上 {}` 让人产生“只能描述对象”的错觉。但确实,联合类型和元组是 interface 只能借用 type 才能写出来的。

为何常见:新手从“interface 用花括号”这个表面特征出发,推导出“只能描述对象”的错误结论,忽略了 interface 内部的多种结构能力。

误区“interface 能声明合并,那我把 type 也写两遍,它是不是也会自动并成一个类型?”

正解不会。同名 type 一旦重复声明,TS 会直接报 Duplicate identifier 错误。能合并的只有 interface(以及 interface 与同名 namespace 的部分合并)。type 的“重复”是错误,interface 的“重复”是设计。把 type 当 interface 用,等于把活页本和印刷卡混为一谈。

为何常见:先学完 interface 的合并特性,很容易把这种“便利”泛化到 type 上,而没有注意到两者的底层设计完全不同。

误区“type 是做类型计算的,它会让 TS 编译变慢,所以项目中尽量用 interface,少用 type。”

正解编译速度的差异几乎不来自“type 这个关键字”,而来自复杂条件类型、递归类型这类高计算量表达式——它们只能写在 type 里。interface 的缓存优化在大型项目里确实有价值,但普通项目可忽略。用 type 写联合类型不会拖慢编译,写千层递归的 DeepPartial 才会。

为何常见:把“复杂类型计算慢”这个现象,错误地归因为“type 关键字慢”,基于表象做了过度概括。

误区“interface 都能继承 type 了,说明 interface 才是完全体,任何时候都该选它。”

正解interface extends type 有一个严格前提:type 必须是对象类型。如果 type 是 string | number 这样的联合类型,extends 直接报错。反过来,type 用交叉类型 & 可以组合包括联合、元组、函数在内的任何类型,自由度反而更高。两者各有边界,不存在谁完全包含谁。

为何常见:看到了“interface extends type“的用法后,误以为这是面向对象继承关系的扩展,没意识到它只是”把对象属性复制一遍”这种受限操作。

误区“编译后都消失了,功能上也差不多,我这次用 interface,下次用 type,反正编译器不报错。”

正解功能上大多等价,但可维护性与协作语义差别很大。interface 表示“可扩展的契约”,type 表示“已算好的配方”。混用会让读者每次都要停下来猜意图,也破坏了公共 API 的可扩展性——如果对象类型用 type 定义,外部团队想通过声明合并追加字段时毫无办法。团队规范的核心是“一致性”。

为何常见:小项目里混写从不报错,于是忽略了代码对“人”的交流价值,只有当多人协作踩坑后才会痛。

面试题

Q1. 关于同名声明合并,以下说法正确的是?

单选题 · easy

  • A.interface 和 type 都会自动合并
  • B.只有 interface 会自动合并← 正确答案
  • C.只有 type 会自动合并
  • D.两者都不会自动合并

答案:B。interface 设计上支持声明合并,多个同名声明自动并成一份;type 同名会报“重复标识符”错误。这就像员工手册可以加页,而复制的配方卡只会造成冲突。

Q2. 要定义一个“字符串或数字”的类型,应该优先选哪个关键字?

单选题 · easy

  • A.interface 关键字
  • B.type 关键字← 正确答案
  • C.class 关键字
  • D.enum 关键字

答案:B。联合类型 string | number 是 type 的独有能力,interface 无法直接表达“要么 A 要么 B”这种结构。type 的等号语法天生适合右边写任意类型表达式。

Q3. interface Admin extends User 表达的正确语义是?

单选题 · mid

  • A.Admin 完全覆盖 User 的定义
  • B.Admin 继承 User 全部属性并可继续新增← 正确答案
  • C.Admin 与 User 的关系变成联合类型
  • D.Admin 只是 User 的一个别名

答案:B。extends 是纵向继承,Admin 自动获得 User 的所有属性,再追加自己的新属性。它不是覆盖、不是联合、也不是别名,而是“在父级基础上扩展”,类似主管手册继承员工手册条款。

Q4. type Mixed = { a: number } & { b: string },Mixed 的类型是?

单选题 · mid

  • A.要么只有 a 属性
  • B.同时拥有 a 和 b 属性← 正确答案
  • C.只有 a 没有 b 属性
  • D.a 和 b 属性互斥出现

答案:B。& 是交叉类型,表示同时满足左右两边,所以 Mixed 必须同时拥有 a 和 b。这就像“土豆烧肉”必须既有土豆又有烧肉,而不是二选一。

Q5. TypeScript 编译为 JavaScript 之后,interface 和 type 会怎样?

单选题 · easy

  • A.变成 JS 对象
  • B.变成函数
  • C.完全消失← 正确答案
  • D.保留为注释

答案:C。类型信息只存在于编译期,编译后所有 interface 与 type 都会被擦除,JS 产物里找不到任何痕迹。它们只负责“编译时检查”,不参与运行时逻辑。

Q6. interface 多次声明相同名字时,每次声明的索引签名类型必须如何?

单选题 · mid

  • A.可以完全不同
  • B.后者覆盖前者
  • C.保持兼容一致← 正确答案
  • D.必须都用 number

答案:C。interface 的声明合并要求里,索引签名需要兼容一致,否则合并时报错。比如一次写 [key: string]: string,另一次写 [key: string]: number,TS 会拒绝合并。而 type 没有合并场景,自然不受此限制。

Q7. 以下哪种 type 无法被 interface extends 扩展?

单选题 · mid

  • A.type Animal = { name: string }
  • B.type A = string | number← 正确答案
  • C.type B = { age: number }
  • D.type C = { list: string[] }

答案:B。interface extends 只能扩展“对象类型的 type”,不能扩展联合类型。string | number 是联合类型,不是一个有属性的对象,无法被 extends。其他选项都是对象形状,可以被继承。

Q8. 以下哪些是 type 独有的表达能力?

多选题 · mid

  • A.联合类型← 正确答案
  • B.声明合并
  • C.条件类型← 正确答案
  • D.元组← 正确答案

答案:ACD。联合类型、条件类型、元组(如 [string, number])都无法用 interface 直接表达,只能写在 type 里。声明合并是 interface 的独有能力,type 同名会报错。这三项加一项合并,正好是两者的核心能力分界线。

Q9. 关于 interface extends type,以下说法正确的有?

多选题 · hard

  • A.只能扩展到对象类型的 type← 正确答案
  • B.不能扩展联合类型← 正确答案
  • C.type 被扩展后可以反向修改
  • D.扩展后类型会自动合并

答案:AB。interface extends type 只是在编译期把 type 的属性“复制”进 interface;type 本身不会改变,不存在“反向修改”;这也不是声明合并,而是继承语义。如果 type 是联合类型则直接报错,所以 A、B 正确。

Q10. 在实际项目中,关于 interface 与 type 的差异,下列描述正确的有?

多选题 · extreme

  • A.interface 可在 d.ts 中通过声明合并补充全局类型← 正确答案
  • B.type 的交叉类型遇到同名属性时编译会直接报错
  • C.interface extends type 时 type 可以是联合类型
  • D.官方推荐“先 interface,不够再用 type”← 正确答案

答案:AD。A 是声明合并的经典用法,全局类型如 Window 可被多次 interface 补充;B 错误,交叉类型遇到同名属性不直接报错,而是把属性变成 never 或难以赋值,错误在真正使用该属性时才暴露;C 错误,interface 不能 extends 联合类型;D 正是 TS 官方手册的原话。