CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
TypeScript 核心/课时/接口与类型别名
RawBlame
课时目录12 节

TypeScript 核心

  1. 01TypeScript 是什么
  2. 02基本类型
  3. 03函数类型
  4. 04接口与类型别名
  5. 05联合与交叉类型
  6. 06类型收窄
  7. 07泛型
  8. 08工具类型
  9. 09模块与 tsconfig
  10. 10React 组件 typing
  11. 11异步与 API 类型
  12. 12综合练习与 Next.js

接口与类型别名

interface、type、extends、对象形状。

35 分钟

接口与类型别名

这一课你将能做到

  1. 用 interface 和 type 描述对象形状,并说清楚两者的适用场景
  2. 正确使用可选属性 ?、只读属性 readonly、索引签名
  3. 用 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' ]

讲清楚要点

  1. 结构化类型系统:TS 只看形状不看名字。interface A { x: number } 和 interface B { x: number } 完全等价,传 A 的地方传 B 没问题。这和 Java 的「名义类型」根本不同。
  2. interface vs type 的口诀:描述对象形状优先 interface;联合、交叉、函数、元组等「组合型」类型用 type。interface 能被类 implements,支持同名声明合并;type 更灵活但不能重复声明。
  3. 可选属性访问必判空:obj.prop?.method() 或 ?? 默认值,strict 模式下编译器逼着你处理 undefined。
  4. readonly 是浅只读:readonly tags: string[] 只锁 tags 这个引用不能换,数组内容还能 push。要深度只读用 Readonly<T> 或 as const。
  5. 索引签名是双刃剑:[key: string] 意味着任何字符串键都合法,拼错的键名也不报错。能用联合字面量做键(Record<"a"|"b", T>)就别用宽索引签名。
  6. 声明合并是 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 让编译器校验

卡点急救

  1. 报错 Property 'x' does not exist on type 'Y':要么是字段名写错(对照接口定义),要么是对象类型太窄(缺字段,需要扩展接口或换更宽的类型)。
  2. 对象字面量报「多余属性」:你多传了接口里没有的字段。确认是笔误就删掉;确实需要就把字段加进接口,或改用类型断言(慎用)。
  3. 接口继承报类型冲突:父子接口同名字段类型不兼容。让子接口字段类型是父接口的子类型,或改用交叉类型看冲突点。
  4. 第三方库类型缺字段:利用 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。

自测三问

  1. 什么时候必须用 type 而不是 interface?举两个场景。
  2. interface A { a: string } 和 type B = { a: string } 在用法上能互换吗?哪种情况下不能?
  3. 为什么把对象先存进变量再传参,就不会触发多余属性检查?这合理吗?

小结一句话

interface 画对象的形状,type 给类型起名字做组合;配上可选、只读、继承、交叉,就能描述项目里 90% 的数据结构。

延伸

  • Handbook: Object Types
  • 下一课:联合与交叉类型——| 表达多选一、字面量联合、判别联合(Discriminated Unions)这个建模利器。
加载本节练习…
← 上一课函数类型下一课 →联合与交叉类型