0027. 交叉类型
- 1. 🎯 本节内容
- 2. 🫧 评价
- 3. 🤔 什么是交叉类型?
- 4. 🆚 交叉类型 vs. 联合类型
- 5. 🤔 同名属性如何交叉?
- 6. 🤔 函数如何交叉?
- 7. 🤔 交叉类型有哪些典型应用场景?
- 8. 🤔 使用交叉类型时需要注意的问题主要是?
- 9. 🤔 同名属性的修饰符如何交叉?【TODO】
- 10. 🔗 引用
1. 🎯 本节内容
- 交叉类型(Intersection Types)
2. 🫧 评价
- 交叉类型表示“且” - 同时满足 A、B、C
- 联合类型表示“或” - 满足 A 或 B 或 C
- 交叉类型使用的时候需要注意,别交叉出 never,这么做通常都是毫无意义的。
3. 🤔 什么是交叉类型?
交叉类型(Intersection Types)是 TypeScript 类型系统中的一个核心特性,使用 &(与符号)将多个类型组合在一起,表示"该值必须同时拥有所有这些类型的属性和方法"。
如果说联合类型表达的是"或"(A 或 B),那么交叉类型表达的就是"与"(A 且 B)——即一个值必须同时满足多个类型的全部要求。
基本语法:交叉类型使用 &(与符号)将多个类型组合在一起。
ts
// 基本用法
type A = { x: string }
type B = { y: number }
type C = A & B
// 等价于 { x: string; y: number; }
const obj: C = {
x: 'hello',
y: 42,
} // ✅ 合法
// ----------------------------------------
// 对象类型的交叉(最常见)
interface Name {
name: string
}
interface Age {
age: number
}
type Person = Name & Age
const alice: Person = {
name: 'Alice',
age: 30,
} // ✅
// ----------------------------------------
// 与原始类型的交叉(较少见,通常无意义)
type Strange = string & number // never(因为没有值能同时是 string 和 number)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
4. 🆚 交叉类型 vs. 联合类型
| 特性 | 交叉类型 (A & B) | 联合类型 (A | B) |
|---|---|---|
| 逻辑 | "且"(同时满足) | "或"(满足其一) |
| 属性 | 包含 A 和 B 的所有属性 | 只能访问 A 和 B 的共有属性 |
| 用途 | 组合能力(Mixins) | 表示多态(状态、可选值) |
| 实例化 | 需提供所有属性 | 只需满足一种形态 |
| 典型场景 | 扩展对象、混入 | 状态机、可空值、多类型参数 |
ts
type A = { x: string }
type B = { y: number }
// 交叉类型:必须同时有 x 和 y
const c: A & B = { x: 'hi', y: 42 }
// 联合类型:可以只有 x 或只有 y
const u1: A | B = { x: 'hi' }
const u2: A | B = { y: 42 }1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
5. 🤔 同名属性如何交叉?
- 情况 1:属性类型兼容 → 合并为更具体的类型
ts
type A = { prop: string }
type B = { prop: 'hello' } // "hello" 是 string 的子类型
type C = A & B // { prop: "hello" }1
2
3
4
2
3
4
- 情况 2:属性类型冲突 → 得到
never
ts
type A = { prop: string }
type B = { prop: number }
type C = A & B // { prop: never } → 整个类型可能无法实例化1
2
3
4
2
3
4
6. 🤔 函数如何交叉?
- 函数也会被合并,定义函数时必须满足所有签名。
ts
type Callable1 = (x: string) => void
type Callable2 = (x: number) => void
type Both = Callable1 & Callable2
// ❌ 错误写法
// const fn: Both = (x: string) => { ... } // ❌ 因为 x 也有可能是 number 类型
// const fn: Both = (x: number) => { ... } // ❌ 因为 x 也有可能是 string 类型
// ✅ 正确
// x 满足了所有的签名。
const fn: Both = (x: string | number) => {
if (typeof x === 'string') {
// handle string
} else {
// handle number
}
}
fn('hello') // ✅
fn(42) // ✅1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
7. 🤔 交叉类型有哪些典型应用场景?
ts
// 混入(Mixins)—— 组合行为
interface Flyable {
fly(): void
}
interface Swimmable {
swim(): void
}
type Duck = { name: string } & Flyable & Swimmable
class MallardDuck implements Duck {
name = 'Mallard'
fly() {
console.log('Flying!')
}
swim() {
console.log('Swimming!')
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts
// 扩展接口(比 extends 更灵活)
type BaseUser = {
id: string
email: string
}
type AdminPermissions = {
role: 'admin'
canDelete: boolean
}
// 创建 AdminUser 类型
type AdminUser = BaseUser & AdminPermissions
const admin: AdminUser = {
id: '1',
email: 'admin@example.com',
role: 'admin',
canDelete: true,
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts
// 高阶函数与泛型中的约束
function extend<T, U>(first: T, second: U): T & U {
return { ...first, ...second }
}
const person = extend({ name: 'Alice' }, { age: 30 })
// person 类型:{ name: string } & { age: number }1
2
3
4
5
6
7
2
3
4
5
6
7
ts
// 与工具类型结合(如 Omit, Pick)
// 为对象添加一个固定属性
type WithId<T> = T & { id: string }
type User = { name: string; email: string }
type UserWithId = WithId<User> // { name: string; email: string; id: string }1
2
3
4
5
6
7
2
3
4
5
6
7
ts
// 条件类型中的交叉
type FlattenIfArray<T> = T extends any[] ? T[number] : T
type ItemType_1 = FlattenIfArray<string[]> // string
type ItemType_2 = FlattenIfArray<number> // number1
2
3
4
2
3
4
ts
// 创建"必填"版本的可选属性
type User = {
name?: string
email?: string
age?: number
isAdmin?: boolean
createdAt?: Date
updatedAt?: Date
}
// 在 User 中 name、email 是可选的
type RequiredUser = User & { name: string; email: string }
// 交叉之后得到的 RequiredUser 强制要求 name 和 email 是必须存在的
const u1: RequiredUser = {
// 如果没有 name 和 email,那么会报错
name: 'Tdahuyou',
email: 'dahuyou_top@163.com',
}
const u2: User = {
// 可以为空,因为 User 的所有属性都是可选的。
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ts
// 与映射类型结合
type ReadonlyAndPartial<T> = Readonly<T> & Partial<T>
// 虽然逻辑上矛盾,但语法合法(实际效果以更严格的为准)
// - Readonly<T> 要求所有属性都是只读的
// - Partial<T> 要求所有属性都是可选的
interface User {
name: string
age: number
}
type UserReadonlyAndPartial = ReadonlyAndPartial<User>
// 等价于:
// {
// readonly name?: string;
// readonly age?: number;
// }
const user: UserReadonlyAndPartial = {
name: 'Alice', // 可选且只读
// age 可以省略
}
// user.name = "Bob"; // ❌ 错误!属性是只读的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
8. 🤔 使用交叉类型时需要注意的问题主要是?
一句话总结 —— 就是别交叉出 never。
- 同名属性冲突。
ts
type X = { a: string };
type Y = { a: number };
type Z = X & Y; // { a: never }
const z: Z = { a: ??? }; // 无法创建合法实例!1
2
3
4
5
2
3
4
5
- 不要滥用交叉原始类型,这没有实际意义,因为原始类型互斥。
ts
type Bad = string & number // never1
- 可辨识联合中慎用交叉,在状态建模中,通常用联合类型而非交叉类型:
ts
// ✅ 正确:状态是互斥的
type State = { type: 'loading' } | { type: 'success'; data: string }
// ❌ 错误:交叉类型要求同时满足 loading 和 success
type WrongState = { type: 'loading' } & { type: 'success'; data: string } // type: never1
2
3
4
5
2
3
4
5
9. 🤔 同名属性的修饰符如何交叉?【TODO】
叠甲
- 注意,demo 中的解释说明,是根据最终测试的行为表现来倒推的,无佐证依据。
- 可复制 demo 丢到 TS Playground 中自行测试。
ts
// 修饰符冲突示例
type ReadonlyAndPartial<T> = Readonly<T> & Partial<T>
// - Readonly<T> 要求所有属性只读
// - Partial<T> 要求所有属性可选
interface User {
name: string
age: number
}
type UserReadonlyAndPartial = ReadonlyAndPartial<User> // 【1】
// type UserReadonlyAndPartial = { readonly name: string; readonly age: number } & { name?: string; age?: number } // 【2】行为表现同【1】
// 【1】推断结果:
// type UserReadonlyAndPartial = Readonly<User> & Partial<User>
// 展开后相当于:{ readonly name: string; readonly age: number } & { name?: string; age?: number }
// ❌ 【1】错误的理解:
// type UserReadonlyAndPartial = {
// readonly name?: string;
// readonly age?: number;
// }
// ✅ 【1】正确理解:
// type UserReadonlyAndPartial = {
// name: string;
// age: number;
// }
// TypeScript 在交叉类型中会合并属性类型,但不会合并修饰符为 readonly + ? 这种组合(属性即是只读、又是可选)。
// 从最终的行为表现来看,TS 是直接将修饰符给丢弃了,只读约束和可选约束都没有起到作用。
// ⚠️ 以下是根据最终行为表现的猜测(没读过 tsc 完整源码):
// 更像是取了修饰符「readonly 只读」、「? 可选」中的共同“特性”
// 只读是 Readonly<T> 特有的“特性”
// 可选是 Partial<T> 特有的“特性”
// 两者之间的交集 ∩,自然就是既不只读、也不可选
// 丢弃 Partial<T>
const user: UserReadonlyAndPartial = {
// name,age 都不可以省略
name: 'Foo',
age: 18,
}
// ❌ 比如,如果省略 age,那么会报错:
// Type '{ name: string; }' is not assignable to type 'UserReadonlyAndPartial'.
// Property 'age' is missing in type '{ name: string; }' but required in type 'Readonly<User>'.(2322)
// 丢弃 Readonly<T>
// 这里不会报错(readonly 未被严格应用)
user.name = 'Bob' // ✅ 可修改(行为宽松)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
以下是 deepwiki 的 Q&A 记录
https://deepwiki.com/search/-type-readonlyandpartialt-read_a096601d-5e4e-40e4-a992-6f483462739a
md
# TypeScript 交叉类型中修饰符的处理机制
你的观察是正确的!TypeScript 在处理 `Readonly<T> & Partial<T>` 时,确实会"丢弃"两个修饰符。这不是 bug,而是交叉类型的设计行为。让我详细解释编译器是如何处理的:
## 1. 工具类型的定义
首先,`Readonly<T>` 和 `Partial<T>` 是通过映射类型定义的: TypeScript:1567-1569 TypeScript:1581-1583
## 2. 交叉类型的属性合并规则
在交叉类型 `A & B` 中,TypeScript 对**同名属性**的修饰符采用了特定的合并逻辑:
### readonly 修饰符的处理
TypeScript 使用 `CheckFlags` 枚举来标记合成属性的特征: TypeScript:6114-6140
关键的测试用例清楚地展示了这个行为: TypeScript:1-25 TypeScript:61-75
**readonly 的合并规则:如果任一成员类型的属性是可变的(非 readonly),则交叉类型的结果属性就是可变的。**
在你的例子中:
- `Readonly<User>` 提供 `readonly name: string`(只读)
- `Partial<User>` 提供 `name?: string`(可变,因为 Partial 不添加 readonly)
- 交叉结果:`name` 是**可变的**(因为有一个成员是可变的)
### 可选修饰符(?)的处理
可选性的逻辑则相反: TypeScript:1-23
**可选性的合并规则:如果任一成员类型的属性是必需的(非可选),则交叉类型的结果属性就是必需的。**
在你的例子中:
- `Readonly<User>` 提供 `readonly name: string`(必需,没有 `?`)
- `Partial<User>` 提供 `name?: string`(可选)
- 交叉结果:`name` 是**必需的**(因为有一个成员要求它必需)
## 3. 为什么是这样的设计?
这个设计符合交叉类型的语义:**一个值必须同时满足所有成员类型的约束**。
- **对于 readonly**:如果一个类型说属性可以修改,那么交叉类型就必须允许修改(否则无法同时满足两个类型)
- **对于可选性**:如果一个类型要求属性必须存在,那么交叉类型就必须要求属性存在(否则不满足那个类型的约束)
## 4. 编译器内部实现
编译器使用 `TransientSymbol`(临时符号)来表示交叉类型中的合成属性,并通过 `CheckFlags` 来追踪这些属性的特征: TypeScript:6142-6150 TypeScript:7173-7176
## Notes
你的猜测"取了修饰符的交集"方向是对的,但更准确的理解是:
- **readonly 修饰符**:任一成员可变 → 结果可变(相当于"或"逻辑)
- **可选修饰符**:任一成员必需 → 结果必需(相当于"且"逻辑)
这就是为什么 `Readonly<T> & Partial<T>` 的结果既不是只读也不是可选,而是回归到了最基本的必需且可变的属性类型。如果你需要同时应用两个修饰符,应该使用嵌套的映射类型,例如 `Readonly<Partial<T>>` 或 `Partial<Readonly<T>>`(它们是等价的)。
### Citations
**File:** src/lib/es5.d.ts (L1567-1569)
```ts
type Partial<T> = {
[P in keyof T]?: T[P]
}
```
**File:** src/lib/es5.d.ts (L1581-1583)
```ts
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
```
**File:** src/compiler/types.ts (L6114-6140)
```ts
export const enum CheckFlags {
None = 0,
Instantiated = 1 << 0, // Instantiated symbol
SyntheticProperty = 1 << 1, // Property in union or intersection type
SyntheticMethod = 1 << 2, // Method in union or intersection type
Readonly = 1 << 3, // Readonly transient symbol
ReadPartial = 1 << 4, // Synthetic property present in some but not all constituents
WritePartial = 1 << 5, // Synthetic property present in some but only satisfied by an index signature in others
HasNonUniformType = 1 << 6, // Synthetic property with non-uniform type in constituents
HasLiteralType = 1 << 7, // Synthetic property with at least one literal type in constituents
ContainsPublic = 1 << 8, // Synthetic property with public constituent(s)
ContainsProtected = 1 << 9, // Synthetic property with protected constituent(s)
ContainsPrivate = 1 << 10, // Synthetic property with private constituent(s)
ContainsStatic = 1 << 11, // Synthetic property with static constituent(s)
Late = 1 << 12, // Late-bound symbol for a computed property with a dynamic name
ReverseMapped = 1 << 13, // Property of reverse-inferred homomorphic mapped type
OptionalParameter = 1 << 14, // Optional parameter
RestParameter = 1 << 15, // Rest parameter
DeferredType = 1 << 16, // Calculation of the type of this symbol is deferred due to processing costs, should be fetched with `getTypeOfSymbolWithDeferredType`
HasNeverType = 1 << 17, // Synthetic property with at least one never type in constituents
Mapped = 1 << 18, // Property of mapped type
StripOptional = 1 << 19, // Strip optionality in mapped property
Unresolved = 1 << 20, // Unresolved type alias symbol
Synthetic = SyntheticProperty | SyntheticMethod,
Discriminant = HasNonUniformType | HasLiteralType,
Partial = ReadPartial | WritePartial,
}
```
**File:** src/compiler/types.ts (L6142-6150)
```ts
/** @internal */
export interface TransientSymbolLinks extends SymbolLinks {
checkFlags: CheckFlags
}
/** @internal */
export interface TransientSymbol extends Symbol {
links: TransientSymbolLinks
}
```
**File:** tests/cases/conformance/types/intersection/intersectionTypeReadonly.ts (L1-25)
```ts
interface Base {
readonly value: number
}
interface Identical {
readonly value: number
}
interface Mutable {
value: number
}
interface DifferentType {
readonly value: string
}
interface DifferentName {
readonly other: number
}
let base: Base
base.value = 12 // error, lhs can't be a readonly property
let identical: Base & Identical
identical.value = 12 // error, lhs can't be a readonly property
let mutable: Base & Mutable
mutable.value = 12
let differentType: Base & DifferentType
differentType.value = 12 // error, lhs can't be a readonly property
let differentName: Base & DifferentName
differentName.value = 12 // error, property 'value' doesn't exist
```
**File:** tests/baselines/reference/intersectionTypeReadonly.types (L61-75)
```text
let mutable: Base & Mutable;
>mutable : Base & Mutable
> : ^^^^^^^^^^^^^^
mutable.value = 12;
>mutable.value = 12 : 12
> : ^^
>mutable.value : number
> : ^^^^^^
>mutable : Base & Mutable
> : ^^^^^^^^^^^^^^
>value : number
> : ^^^^^^
>12 : 12
> : ^^
```
**File:** tests/cases/compiler/intersectionsAndOptionalProperties.ts (L1-23)
```ts
// @strict: true
declare let x: { a?: number; b: string }
declare let y: { a: null; b: string }
declare let z: { a: null } & { b: string }
x = y // Error
x = z // Error
// Repro from #36604
interface To {
field?: number
anotherField: string
}
type From = { field: null } & Omit<To, 'field'>
function foo(v: From) {
let x: To
x = v // Error
x.field = v.field // Error
}
```
**File:** src/compiler/utilities.ts (L7173-7176)
```ts
/** @internal */
export function hasEffectiveReadonlyModifier(node: Node): boolean {
return hasEffectiveModifier(node, ModifierFlags.Readonly)
}
```1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
md
# DeepWiki Q&A with Code Context for Repository: microsoft/TypeScript
https://deepwiki.com/search/-type-readonlyandpartialt-read_a096601d-5e4e-40e4-a992-6f483462739a
```ts
// 修饰符冲突示例
type ReadonlyAndPartial<T> = Readonly<T> & Partial<T>
// - Readonly<T> 要求所有属性只读
// - Partial<T> 要求所有属性可选
interface User {
name: string
age: number
}
type UserReadonlyAndPartial = ReadonlyAndPartial<User> // 【1】
// type UserReadonlyAndPartial = { readonly name: string; readonly age: number } & { name?: string; age?: number } // 【2】行为表现同【1】
// 【1】推断结果:
// type UserReadonlyAndPartial = Readonly<User> & Partial<User>
// 展开后相当于:{ readonly name: string; readonly age: number } & { name?: string; age?: number }
// ❌ 【1】错误的理解:
// type UserReadonlyAndPartial = {
// readonly name?: string;
// readonly age?: number;
// }
// ✅ 【1】正确理解:
// type UserReadonlyAndPartial = {
// name: string;
// age: number;
// }
// TypeScript 在交叉类型中会合并属性类型,但不会合并修饰符为 readonly + ? 这种组合(属性即是只读、又是可选)。
// 从最终的行为表现来看,TS 是直接将修饰符给丢弃了,只读约束和可选约束都没有起到作用。
// ⚠️ 以下是根据最终行为表现的猜测(没读过 tsc 源码):
// 更像是取了修饰符「readonly 只读」、「? 可选」中的共同“特性”
// 只读是 Readonly<T> 特有的“特性”
// 可选是 Partial<T> 特有的“特性”
// 两者之间的交集 ∩,自然就是既不只读、也不可选
// 丢弃 Partial<T>
const user: UserReadonlyAndPartial = {
// name,age 都不可以省略
name: 'Foo',
age: 18,
}
// ❌ 比如,如果省略 age,那么会报错:
// Type '{ name: string; }' is not assignable to type 'UserReadonlyAndPartial'.
// Property 'age' is missing in type '{ name: string; }' but required in type 'Readonly<User>'.(2322)
// 丢弃 Readonly<T>
// 这里不会报错(readonly 未被严格应用)
user.name = 'Bob' // ✅ 可修改(行为宽松)
```
你能解释 TS 编译器是如何处理这种情况的吗?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60