这节在地图里是"语言结构"的入口。新手最常问:定义对象形状,用 interface 还是 type?两者 90% 重叠,但剩下 10% 的差别决定了可读性。我们给你一条清晰的选型线。
interface UserI { id: number; name: string; } type UserT = { id: number; name: string; }; const a: UserI = { id: 1, name: "张三" }; const b: UserT = { id: 2, name: "李四" };
输入输出:两种写法下 a、b 用法完全一致。在仅描述对象字段时,二者等价,团队统一选一种即可。
接口的"开放扩展"特性在给第三方类型补字段时很有用。
// 同一作用域重复声明 interface,自动合并 interface Window { myConfig: string; } interface Window { theme: "dark" | "light"; } // 合并后 Window 同时有 myConfig 和 theme
类型别名一旦声明就锁死,重复声明会报错。所以当你想"增量扩展一个形状"(尤其给全局对象如 window 加属性),接口更顺手。
type 不限于对象,它能给任意类型起名,包括联合、元组、函数签名。
type Id = string | number; // 联合,interface 做不到 type Pair = [number, number]; // 元组 type Handler = (e: Event) => void; // 函数类型 type Maybe<T> = T | null; // 泛型别名
背景:想给"几种可能"或"函数签名"起个名字,interface 语法不支持。操作:用 type。结果:一个名字涵盖非对象类型。解读:这是 type 的核心优势区。
interface A { x: number; } interface B extends A { y: number; } // 接口用 extends type C = { x: number; }; type D = C & { y: number }; // 类型用交叉 &
extends 与 & 在这里效果相同。接口继承只能继承接口;交叉能拼任意类型,包括基本类型(如 string & { __brand } 品牌类型,见 2.1)。
这张图把"何时用哪个"做成决策流:

interface CreateOrderInput { productId: string; quantity: number; } type CreateOrderResult = | { ok: true; orderId: string } | { ok: false; reason: string }; // 输入用 interface,结果用 type(联合),各取所长
当对象的键不固定但值同构时,用索引签名描述,接口与类型别名都能写。
interface StringMap { [key: string]: string; } const env: StringMap = { token: "abc", region: "cn" }; const v = env["token"]; // string // 想同时保留已知字段 + 任意键,可混用 interface Config { version: number; // 已知固定字段 [key: string]: string | number; // 其余键允许 string 或 number }
背景:字典、环境配置、国际化文案常是"键不固定"。操作:用 [key: K]: V 索引签名。结果:取值得到统一类型 V。解读:索引签名的值类型必须覆盖所有显式字段的类型,否则报错——这是初学者常踩的不一致点。
readonly 修饰符的位置差异readonly 在接口与类型别名里都能用,但位置语义一致:锁住字段不可重新赋值。
interface Point { readonly x: number; readonly y: number; } type Vec = readonly [number, number]; // 只读元组,类似 const 元组 const p: Point = { x: 1, y: 2 }; // p.x = 3; // 报错:只读 const v: Vec = [1, 2]; // v[0] = 3; // 报错:只读元组
接口里 readonly 锁字段,类型别名里 readonly 前缀在元组上能锁住下标。两者都把"不可变"写进契约,比靠注释提醒可靠。
接口和类型别名都支持泛型参数,表达"参数化形状"时写法略有不同,但消费端一致。
interface BoxI<T> { value: T; } type BoxT<T> = { value: T }; const i: BoxI<number> = { value: 1 }; const t: BoxT<string> = { value: "a" };
若需要"带方法的容器",接口能直接声明方法签名,类型别名得用箭头函数字段,接口更贴近面向对象直觉:
interface Repo<T> { get(id: number): T; save(item: T): void; } type RepoT<T> = { get: (id: number) => T; save: (item: T) => void; };
两者都能表达递归结构(如树、链表),写法等价。
interface TreeNode { value: number; children: TreeNode[]; // 自引用 } type Tree = { value: number; children: Tree[]; };
注意:递归类型若用 type 配条件类型会进入更复杂的展开逻辑(第五章),但纯对象递归无论接口还是类型别名都稳定支持。
单一风格最省认知负担。很多团队规定"对象形状一律 interface,非对象或联合一律 type",把选型成本降为零。我们倾向这条线,但它不是强制;真正要避免的是同一个项目里毫无理由地交替,让后人每次都要纠结。
interfacetypeextends vs &,语义一致⚠️ 别以为 type 是 interface 的升级替代。声明合并这个能力只有 interface 有,需要给全局或第三方类型补字段时,type 无能为力。
💡 若团队无法达成一致,采用"对象用 interface、其余用 type"的硬规则,能消除绝大多数无谓争论,也不损失表达力。