前置回顾
- 第 02 篇讲了基础类型(
number,string,boolean等)和类型注解语法 - 第 03 篇讲了函数参数和返回值的类型标注
- 本篇进入对象: TS 中最常用的复合类型, 以及用
interface和type给对象形状命名
一句话总结
interface 和 type 都能描述对象的形状(有哪些字段, 每个字段什么类型). 对象结构优先用 interface, 联合类型和工具类型用 type. TS 是结构化类型系统: 只要形状匹配就兼容, 不需要显式声明"实现了某接口".
对象类型基础
最直接的方式: 内联写出对象的形状:
|
对比 Go 和 Rust:
|
|
TS 的对象类型更像是"匿名 struct", 可以直接写在参数位置, 不需要提前定义.
可选属性
属性名后加 ?, 表示可以不存在:
|
只读属性
readonly 防止属性被修改:
|
对比: Rust struct 字段默认不可变(除非 &mut), Go struct 没有字段级不可变语法.
接口(interface)
当对象类型需要复用时, 用 interface 给它命名:
|
接口继承
用 extends 扩展已有接口, 类似 Go 的接口嵌入:
|
支持多继承:
|
接口合并(Declaration Merging)
同名 interface 自动合并为一个:
|
这是 interface 独有的能力, type 不能做到. 主要用于扩展第三方库的类型定义.
对比 Go 和 Java 的 interface
| 语言 | interface 描述的是 | 实现方式 |
|---|---|---|
| Go | 行为(方法集合) | 隐式实现, 只要有对应方法就满足 |
| Java | 行为(方法签名) | 显式 implements |
| TypeScript | 形状(属性 + 方法) | 隐式满足, 只要结构匹配 |
Go 的 interface = 职位要求(会写代码, 会沟通), 不管从哪毕业.
TS 的 interface = 物品清单(有名字, 有年龄, 有邮箱), 不管这个对象从哪来.
类型别名(type)
type 给任何类型起一个名字:
|
type 能做但 interface 不能做的事
|
interface vs type: 如何选择
| 能力 | interface |
type |
|---|---|---|
| 描述对象形状 | 支持 | 支持 |
| extends 继承 | 支持 | 用 & 交叉实现 |
| 声明合并 | 支持 | 不支持 |
| 联合类型 | 不支持 | 支持 |
| 映射类型 | 不支持 | 支持 |
| 元组 | 不支持 | 支持 |
| class implements | 支持 | 支持(仅对象形状) |
选择策略
- 对象结构: 优先
interface(可继承, 可合并, IDE 提示更友好) - 联合类型, 工具类型, 元组: 必须用
type - 两者都行时: 团队统一即可, 不必纠结
社区趋势: React 生态偏好 type, Node.js/后端库偏好 interface. 选一个坚持就好.
索引签名
当对象的 key 不确定, 用索引签名描述"任意键":
|
对比 Go:
|
Record 工具类型
Record<K, V> 是索引签名的语法糖:
|
嵌套对象与可选链
实际项目中对象经常深层嵌套:
|
访问深层可选属性时, 用 ?. 可选链(短路求值, 遇到 null/undefined 返回 undefined):
|
空值合并操作符 ??
?? 在左侧为 null 或 undefined 时返回右侧值:
|
对比 ||: || 在左侧为任何 falsy 值(0, "", false)时都会走右侧, ?? 只在 null/undefined 时.
|
对比其他语言:
| 语言 | 空值安全访问 | 默认值 |
|---|---|---|
| Go | if x != nil { x.Field } |
无语法糖, 手动判断 |
| Rust | x.as_ref()?.field / x.map(...) |
.unwrap_or(default) |
| TypeScript | x?.field |
x ?? default |
结构化类型系统(鸭子类型)
TS 的类型兼容性基于结构(形状), 不基于名字:
|
只要一个对象拥有接口要求的所有属性, 它就满足该接口. 多余的属性不影响:
|
结构化类型 = 看能力不看出身. 只要会游泳就能参加游泳比赛, 不管是人还是鸭子.
名义类型(Java/Rust) = 看证书. 必须显式声明 implements Swimmer 才行.
对比:
| 语言 | 类型系统 | 接口满足方式 |
|---|---|---|
| Go | 结构化(方法) | 隐式, 有方法就满足 |
| TypeScript | 结构化(属性 + 方法) | 隐式, 形状匹配就满足 |
| Rust | 名义 | 显式 impl Trait for Type |
| Java | 名义 | 显式 implements Interface |
多余属性检查
结构化类型的一个例外: 直接传字面量时, TS 会检查多余属性:
|
为什么字面量有特殊检查
直接传字面量时, 多余的属性几乎总是拼写错误(如把 colour 写成了 color). TS 的多余属性检查是一个实用的防错机制, 但它只在字面量赋值时触发.
实用工具类型
TS 内置了一组工具类型, 基于已有类型生成新类型:
|
这些工具类型的实现原理(映射类型 + 条件类型)在阶段三详细讲. 现在只需要会用.
Partial 的典型使用场景
更新操作: 只传需要修改的字段, 其余保持不变.
|
Pick 和 Omit 哪个更好
字段少时用 Pick(白名单), 字段多时用 Omit(黑名单). 选让代码更短的那个.
但 Pick 在源接口添加字段时不会自动带入, 更安全; Omit 会自动带入新字段, 可能无意暴露数据.
快速回顾
- 对象类型: 内联写
{ key: type }, 支持可选(?)和只读(readonly)属性 - interface: 给对象形状命名, 支持继承(
extends)和声明合并 - type: 给任何类型命名, 支持联合, 交叉, 映射, 元组
- 选择策略: 对象结构用 interface, 联合/工具类型用 type
- 索引签名:
[key: string]: T描述动态键, 或用Record<K, V> - 可选链:
?.安全访问深层属性,??提供空值默认值 - 结构化类型: 形状匹配就兼容, 不需要显式声明实现关系
- 工具类型:
Partial,Pick,Omit,Readonly基于已有类型派生新类型
动手练习
- 定义接口: 为一个博客系统定义
Post,Author,Comment三个接口, 建立它们之间的关系(Post 有 author 和 comments). - interface 继承: 定义
BaseEntity(id, createdAt, updatedAt), 让 Post 和 Author 都继承它. - type vs interface: 用
type定义一个ApiResponse<T> = { data: T; error: string | null }泛型类型, 思考为什么这里用 type 更合适. - 可选链练习: 给定一个深层嵌套的配置对象, 用
?.和??安全地读取各级属性并提供默认值. - 鸭子类型验证: 定义两个不同名但形状相同的 interface, 验证它们之间可以互相赋值.
- 工具类型组合: 用
Omit+Partial组合出一个"创建 Post"的 DTO 类型: id 不需要传, 其余字段可选.