CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
接口与类型别名
这一课你将能做到
用 interface 和 type 描述对象形状,并说清楚两者的适用场景
正确使用可选属性 ?、只读属性 readonly、索引签名
用 extends 继承和交叉类型 & 组合出复杂类型
为什么要学
JavaScript 里万物皆对象:API 返回的数据是对象、组件的 props 是对象、配置文件是对象。TS 的核心价值就是给这些对象「画形状」——这个对象必须有哪些字段、每个字段什么类型、哪些可缺省。
interface 和 type 就是画形状的两支笔。实际开发中 80% 的类型工作都是在写它们:定义后端返回的 DTO、定义表单的字段结构、定义组件的 props。这一课学会了,你就能读懂项目里绝大多数类型代码。
先建立图像
接口就是对象的「合同」:
合同(interface User) 签约方(实现该类型的对象)
┌─────────────────┐ ┌──────────────────────────┐
│ id: number │ ◀───── │ { id: 1, name: "小明", │
│ name: string │ 必须符合 │ email: "a@b.com" } │
│ email: string │ │ ✓ 字段齐全、类型匹配 │
└─────────────────┘ └──────────────────────────┘
│
{ id: 1 } │ ✗ 缺字段,编译报错
一句话:接口只关心形状,不关心来源 。只要对象长对了样子,TS 就认——这叫「结构化类型」(鸭子类型:走起来像鸭子、叫起来像鸭子,那就是鸭子)。
跟着做
新建 interfaces-demo.ts,逐步跟练。
步骤 1:第一个接口
interface User {
id: number;
name: string;
email: string;
}
function printUser(user: User): void {
console.log(`#${user.id} ${user.name} <${user.email}>`);
}
printUser({ id: 1, name: "小明", email: "ming@example.com" });
// #1 小明 <ming@example.com>
// printUser({ id: 1, name: "小明" }); // 报错:缺少 email
// printUser({ id: "1", name: "小明", email: "x" }); // 报错:id 类型不对
步骤 2:可选属性与只读属性
interface Article {
id: number;
title: string;
readonly createdAt: string; // 只读:赋值后不能改
summary?: string; // 可选:可以没有
tags?: string[];
}
const article: Article = {
id: 1,
title: "TypeScript 入门",
createdAt: "2026-10-06",
// summary 和 tags 都可以不写
};
article.title = "新标题"; // OK
// article.createdAt = "2026-01-01"; // 报错:readonly 不可修改
// 访问可选属性要处理 undefined
console.log(article.summary?.toUpperCase() ?? "暂无摘要"); // 暂无摘要
步骤 3:接口里的方法
interface Counter {
count: number;
increment(): void; // 方法签名
reset(to?: number): void; // 带可选参数的方法
}
const counter: Counter = {
count: 0,
increment() {
this.count++;
},
reset(to = 0) {
this.count = to;
},
};
counter.increment();
counter.increment();
console.log(counter.count); // 2
counter.reset(10);
console.log(counter.count); // 10
步骤 4:索引签名——字段名不固定的对象 // 场景:翻译字典、动态配置,键名事先不知道
interface TranslationMap {
[key: string]: string; // 任意字符串键,值必须是 string
}
const zhCN: TranslationMap = {
hello: "你好",
goodbye: "再见",
welcome: "欢迎",
};
console.log(zhCN["hello"]); // 你好
// 索引签名可以和固定属性共存,但固定属性类型必须兼容索引签名
interface Config {
[key: string]: string | number;
version: string; // OK:string 兼容 string | number
maxRetry: number; // OK
}
const config: Config = { version: "1.0", maxRetry: 3, theme: "dark" };
步骤 5:接口继承 interface Entity {
id: number;
createdAt: string;
}
// extends 继承:子接口自动拥有父接口所有字段
interface User extends Entity {
name: string;
email: string;
}
interface Post extends Entity {
title: string;
authorId: number;
}
const user: User = {
id: 1,
createdAt: "2026-01-01",
name: "小明",
email: "ming@example.com",
};
// 可以多继承
interface Timestamps {
createdAt: string;
updatedAt: string;
}
interface Record extends Entity, Timestamps {
data: string;
}
步骤 6:type 类型别名 // type 可以给任何类型起名字,不只是对象
type ID = number | string;
type Status = "active" | "inactive" | "banned";
interface Account {
id: ID;
status: Status;
}
// type 也能描述对象,写法几乎和 interface 一样
type Point = {
x: number;
y: number;
};
const p: Point = { x: 1, y: 2 };
步骤 7:交叉类型 & —— type 的组合方式 interface HasName {
name: string;
}
interface HasAge {
age: number;
}
// 交叉类型:同时满足两个形状
type Person = HasName & HasAge;
const person: Person = { name: "小红", age: 28 };
// 等价于接口继承的效果,但用的是组合思维
function introduce(p: Person): string {
return `${p.name},${p.age} 岁`;
}
console.log(introduce(person)); // 小红,28 岁
步骤 8:interface vs type 决策实战 // 场景一:定义对象/DTO —— 推荐 interface(报错信息更清晰,支持声明合并)
interface CourseDTO {
id: number;
title: string;
price: number;
}
// 场景二:联合类型、函数类型、元组 —— 只能用 type
type Role = "admin" | "editor" | "viewer";
type Handler = (event: string) => void;
type Point3D = [number, number, number];
// 场景三:需要交叉组合 —— 用 type + &
type AdminCourse = CourseDTO & { permissions: string[] };
const course: AdminCourse = {
id: 1,
title: "TS 实战",
price: 99,
permissions: ["read", "write"],
};
console.log(course.title, course.permissions); // TS 实战 [ 'read', 'write' ]
讲清楚要点
结构化类型系统 :TS 只看形状不看名字。interface A { x: number } 和 interface B { x: number } 完全等价,传 A 的地方传 B 没问题。这和 Java 的「名义类型」根本不同。
interface vs type 的口诀 :描述对象形状优先 interface;联合、交叉、函数、元组等「组合型」类型用 type。interface 能被类 implements,支持同名声明合并;type 更灵活但不能重复声明。
可选属性访问必判空 :obj.prop?.method() 或 ?? 默认值,strict 模式下编译器逼着你处理 undefined。
readonly 是浅只读 :readonly tags: string[] 只锁 tags 这个引用不能换,数组内容还能 push。要深度只读用 Readonly<T> 或 as const。
索引签名是双刃剑 :[key: string] 意味着任何字符串键都合法,拼错的键名也不报错。能用联合字面量做键(Record<"a"|"b", T>)就别用宽索引签名。
声明合并是 interface 独有 :同名 interface 写两次会自动合并字段——给第三方库打补丁(declaration merging)靠的就是这个特性。
你可能会懵的地方
为什么我对象字面量多传一个字段也报错?
这叫「多余属性检查」,只在对象字面量直接赋值 时触发,防止你写错字段名。先把对象存进变量再传入,检查就放宽了(因为形状依然兼容)。
接口能描述函数吗?
能:interface Fn { (a: number): string },但实践中都用 type Fn = (a: number) => string,可读性更好。
type A = B 是复制了一份吗?
不是复制,是起了个别名。A 和 B 是同一个类型的两个名字,改 B 的定义 A 跟着变(本来就是同一个东西)。
接口继承和交叉类型结果一样吗?
字段层面几乎一样。差别在冲突处理:interface A extends B 字段类型冲突会直接报错;A & B 冲突时会合成 never(不可能的类型),报错时机更晚更难查。
可选属性和 | undefined 是一回事吗?
prop?: string = prop: string | undefined 且允许「整个键不存在」。开启了 exactOptionalPropertyTypes 严格配置后两者会有差别:? 允许缺省但不允许显式赋 undefined……日常开发不用纠结,知道两者近乎等价即可。
错法对照 错法 问题 正解 所有类型都用 type 或都用 interface 丧失各自优势 对象形状用 interface,组合类型用 type obj.summary.length 直接访问可选属性运行时可能 undefined.length 崩溃 obj.summary?.length 或先判空以为 readonly 数组内容也不可变 浅只读,push 照样成功 需要深度只读用 Readonly<T> 滥用 [key: string]: any 键名拼错不报错,类型全废 键集合已知就用字面量联合做键 类忘了 implements 接口 接口形同虚设,类写歪了不自知 class X implements Y 让编译器校验
卡点急救
报错 Property 'x' does not exist on type 'Y' :要么是字段名写错(对照接口定义),要么是对象类型太窄(缺字段,需要扩展接口或换更宽的类型)。
对象字面量报「多余属性」 :你多传了接口里没有的字段。确认是笔误就删掉;确实需要就把字段加进接口,或改用类型断言(慎用)。
接口继承报类型冲突 :父子接口同名字段类型不兼容。让子接口字段类型是父接口的子类型,或改用交叉类型看冲突点。
第三方库类型缺字段 :利用 interface 声明合并,在同名 declare module "xxx" 里补字段;或在项目 *.d.ts 里扩展。
面试常问
interface 和 type 的区别?
核心三点:① interface 只描述对象/类形状,type 可以给任何类型起别名(联合、元组、函数等);② interface 支持声明合并(同名自动合并),type 不行;③ interface 用 extends 继承,type 用 & 交叉。实践约定:对象/DTO 用 interface,组合类型用 type。
TS 的类型系统是结构化还是名义化的?
结构化(鸭子类型)。只看形状匹配,不看类型名字。所以两个独立定义但形状相同的接口可以互换使用。
readonly 能实现不可变数据吗?
只能实现浅层只读——锁定属性引用本身不被重新赋值,对象内部、数组元素仍可修改。要完全不可变需要递归 readonly(Readonly<T> 工具类型,后续课讲)或运行时手段如 Object.freeze。
什么是声明合并?有什么用?
同名 interface 多次声明会自动合并成一个。典型用途:给第三方库的全局类型打补丁,比如扩展 Window 对象、扩展 Express 的 Request。
自测三问
什么时候必须用 type 而不是 interface?举两个场景。
interface A { a: string } 和 type B = { a: string } 在用法上能互换吗?哪种情况下不能?
为什么把对象先存进变量再传参,就不会触发多余属性检查?这合理吗?
小结一句话 interface 画对象的形状,type 给类型起名字做组合;配上可选、只读、继承、交叉,就能描述项目里 90% 的数据结构。
延伸