0072. 类型的父子关系
- 1. 🎯 本节内容
- 2. 🫧 评价
- 3. 🧠 核心原则
- 4. 🤔 类型之间的父子关系有什么用?
- 5. 🤔 如果给你两个类型,让你判断它们的父子关系,你应该怎么判断呢?
- 6. 🤔 “字面量类型”和“原始类型”的父子关系是?
- 7. 🤔 “联合子集”和“联合超集”的父子关系是?
- 8. 🤔 “可空类型”和“非空类型”的父子关系是?
- 9. 🤔 “对象子集”和“对象超集”的父子关系是?
- 10. 🤔 “可变数组”和“只读数组”的父子关系是?
1. 🎯 本节内容
- 类型的父子关系
2. 🫧 评价
我们经常会看到类似下面这样的描述:
- 某某类型更宽泛
- 某某类型更具体
- 某某类型更大
- 某某类型更小
- 某某是父类型
- 某某是子类型
- ……
类似的这些描述,通常都是在说类型之间的父子关系。搞清楚如何辨别类型之间的父子关系,就是本节笔记要介绍的具体内容。
3. 🧠 核心原则
本节要介绍的父子类型之间的关系有点儿绕,记住核心原则可以帮我们快速判断类型之间的父子关系。
- 子类型更具体且能力更强
- 父类型更宽泛且能力更弱
- 子可以“冒充”父,但是父不能“冒充”子,因此
父类型 = 子类型 // ✅ 兼容、子类型 = 父类型 // ❌ 不兼容
4. 🤔 类型之间的父子关系有什么用?
类型之间的父子关系决定了类型之间的赋值关系,也就是类型的兼容性。
如果我们已知 A 是 B 的子类型,那么可以推断出哪些结论呢?
A 是 B 的子类型 => A ⊆ B => A 可以安全替换 B- 在 TS 看来,B 比 A 要多一些特性;
- A 要的 B 都有,因此把 A 赋给 B 是允许的;
- B 要的 A 不一定有,因此把 B 赋值给 A 是不允许的;
ts
父类型 = 子类型 // ✅ 兼容
子类型 = 父类型 // ❌ 不兼容1
2
2
判断父子类型的粗略标准:如果一个类型 A 的值可以在所有需要另一个类型 B 的地方被安全使用,那么 A 就是 B 的子类型。
5. 🤔 如果给你两个类型,让你判断它们的父子关系,你应该怎么判断呢?
判断思路:
- 先从角度 1 判断
- 角度 1 判断不了再考虑角度 2
- 或者直接根据个人经验“父子关系模式”来直接套
5.1. 角度 1:从“集合”角度来判断
从“集合”角度来判断是比较直观的,也更符合官方采用的“结构子类型”的策略。
判断规则:
- 子类型小
- 父类型大
简单来说就是大的包含小的,比如:
string比"hello"大 =>"hello"是string的子类型string | undefined比string大 =>string是string | undefined的子类型
5.2. 角度 2:从“能力”角度来判断
很多时候我们无法直观的通过“集合”的角度来判断,比如 可选 和 只读 哪个大哪个小呢?这时候就可以尝试从“能力”的角度来判断。
判断规则:
- 子类型能力强,你可以对它进行更多的操作,更自由。
- 父类型能力弱,你能做的操作更有限,更约束。
比如:
- 能读能写的
string[]是子类型,只读的readonly string[]是父类型。 string是子类型,你可以读取它的长度str.length、访问字符串身上的方法str.toUpperCase()等完成字符串类型的其它操作;string | undefined是父类型。- ……
注意事项:
这里的“能力”指的是从开发者角度来看的,而不是从类型角度来看。
- 比如下面这样的辨别角度就是经典的错误:
- ❌
readonly string[]比string[]多了readonly的功能,所以它的能力更强,因此readonly string[]是string[]的子类型。 - 能力不是从类型角度来看的,而是从开发者角度来看的。
5.3. 讨论父子类型的必要场景
当且仅当两个类型之间有交集时,才有讨论父子类型的必要,否则是没有意义的。
- 情况 1:string 和 number 就没有讨论父子类型的必要,它们值域完全不相交,不可能互相赋值;
- 情况 2:123 和 number 的值域就有可能相交,123 是 number 的子类型;
- 类似情况 1 和情况 2 的场景还有很多,笔记中会尽可能列举出开发中常见的一些情况。
5.4. 常见的父子关系模式
| 模式 | 子类型 → 父类型 |
|---|---|
| 字面量 → 原始类型 | "a" → string |
| 联合子集 → 联合超集 | A → A | B |
| 对象超集 → 对象子集 | {x,y,z} → {x,y} |
| 可变数组 → 只读数组 | T[] → readonly T[] |
| 非空类型 → 可空类型 | T → T | undefined |
| 派生类 → 基类 | Dog → Animal |
| 函数(参数更宽、返回更窄)→ 函数(参数更窄、返回更宽) | - |
5.5. 不存在父子关系的情况
stringvsnumberstring[]vsnumber[]{ a: string }vs{ b: number }(x: string) => voidvs(x: number) => void- ……
- 类似的情况还有很多,类似上面这样的不存在父子关系的示例还是比较好辨别的;
6. 🤔 “字面量类型”和“原始类型”的父子关系是?
字面量类型是其对应宽泛类型的子类型。
txt
42 ⊆ number
"hello" ⊆ string
true ⊆ boolean1
2
3
2
3
"hello"可以用在任何需要string的地方 →"hello"是string的子类型。- 但
string不能用在需要"hello"的地方。
7. 🤔 “联合子集”和“联合超集”的父子关系是?
如果类型 A 的所有成员都包含在类型 B 中,则 A 是 B 的子类型。
txt
string ⊆ string | number
string | number ⊆ string | number | boolean1
2
2
8. 🤔 “可空类型”和“非空类型”的父子关系是?
这个问题其实跟【“联合子集”和“联合超集”的父子关系】是一个问题。
可空其实就是联合上一个 undefined。
- 非空类型小,是子类型
- 可空类型大,是父类型
txt
T ⊆ T | undefined
T ⊆ T | null
T ⊆ T | null | undefined1
2
3
2
3
9. 🤔 “对象子集”和“对象超集”的父子关系是?
⚠️ 注意
这是很容易混淆的一个点,不要误以为多的就是父,少的就是子。
回顾一下核心原则:
- 子类型更具体且能力更强
- 父类型更宽泛且能力更弱
- 子可以“冒充”父,但是父不能“冒充”子,因此
父类型 = 子类型 // ✅ 兼容、子类型 = 父类型 // ❌ 不兼容
具体 ≠ 少、宽泛 ≠ 多,真实情况恰恰是反过来:
- 属性越多,越具体,是子类型;
- 属性越少,越宽泛,是父类型;
TypeScript 是结构类型系统:只要 A 包含 B 所需的所有属性(且类型兼容),A 就是 B 的子类型。
ts
interface Point {
x: number
y: number
}
interface Point3D {
x: number
y: number
z: number
}
let p: Point = { x: 1, y: 2 } // ✅ OK
let p3: Point3D = { x: 1, y: 2, z: 3 } // ✅ OK
// Point3D 有更多属性,但满足 Point 的结构
p = p3 // ✅ OK
// Point 中没有 Point3D 要的 z
// p3 = p // ❌ 不兼容
// Property 'z' is missing in type 'Point' but required in type 'Point3D'.(2741)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Point3D是Point的子类型(多出的属性被忽略)。- 注意:这是“超集对象是子类型”,因为满足更多要求。
对象子类型:子类型 = 结构超集(属性更多或兼容)
10. 🤔 “可变数组”和“只读数组”的父子关系是?
- 可变数组“能力”强,是子类型;
- 只读数组“能力”弱,是父类型;
txt
string[] ⊆ readonly string[]
number[] ⊆ readonly number[]1
2
2
类比理解:
- 可读写的编辑器你可以将其当做只读编辑器用
- 只读编辑器你无法将其视作可读写的编辑器用