联合类型和交叉类型是 TypeScript 中组合类型的两种基本方式。联合类型表示”或”的关系,交叉类型表示”且”的关系。掌握这两种类型是理解 TypeScript 类型系统的关键。
联合类型#
联合类型使用 | 表示一个值可以是多种类型之一:
// TypeScript 5.x
// 基础联合类型let value: string | number
value = 'hello' // ✅value = 42 // ✅// value = true; // ❌ boolean 不在联合类型中
// 函数参数使用联合类型function formatId(id: string | number): string { return `ID: ${id}`}
formatId(123) // "ID: 123"formatId('abc') // "ID: abc"联合类型的访问限制#
联合类型只能访问所有类型共有的成员:
function getLength(value: string | number) { // ❌ 错误:number 没有 length 属性 // return value.length;
// ✅ toString() 是共有方法 return value.toString().length}
// 需要类型收窄才能访问特定类型的成员function getLength2(value: string | number): number { if (typeof value === 'string') { return value.length // ✅ 这里 value 是 string } return value.toString().length // 这里 value 是 number}类型收窄#
TypeScript 通过控制流分析自动收窄联合类型:
typeof 收窄#
function process(value: string | number | boolean) { if (typeof value === 'string') { // value: string console.log(value.toUpperCase()) } else if (typeof value === 'number') { // value: number console.log(value.toFixed(2)) } else { // value: boolean console.log(value ? 'yes' : 'no') }}instanceof 收窄#
class Dog { bark() { console.log('Woof!') }}
class Cat { meow() { console.log('Meow!') }}
function speak(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark() // animal: Dog } else { animal.meow() // animal: Cat }}in 操作符收窄#
interface Fish { swim(): void}
interface Bird { fly(): void}
function move(animal: Fish | Bird) { if ('swim' in animal) { animal.swim() // animal: Fish } else { animal.fly() // animal: Bird }}真值收窄#
function printMessage(message: string | null | undefined) { if (message) { // message: string(排除了 null、undefined 和空字符串) console.log(message.toUpperCase()) } else { console.log('No message') }}可辨识联合#
可辨识联合是联合类型的强大模式,通过共同的字面量属性来区分类型:
// 定义带有 kind 属性的类型interface Circle { kind: 'circle' radius: number}
interface Rectangle { kind: 'rectangle' width: number height: number}
interface Triangle { kind: 'triangle' base: number height: number}
type Shape = Circle | Rectangle | Triangle
// 根据 kind 属性收窄类型function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2 case 'rectangle': return shape.width * shape.height case 'triangle': return (shape.base * shape.height) / 2 }}
// 使用const circle: Circle = { kind: 'circle', radius: 10 }console.log(getArea(circle)) // 314.159...穷尽检查#
type Shape = Circle | Rectangle | Triangle
function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2 case 'rectangle': return shape.width * shape.height case 'triangle': return (shape.base * shape.height) / 2 default: // 穷尽检查:如果所有情况都处理了,shape 是 never const _exhaustiveCheck: never = shape return _exhaustiveCheck }}
// 如果新增类型但忘记处理,TypeScript 会报错interface Square { kind: 'square' size: number}
type Shape2 = Circle | Rectangle | Triangle | Square
function getArea2(shape: Shape2): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2 case 'rectangle': return shape.width * shape.height case 'triangle': return (shape.base * shape.height) / 2 // 缺少 'square' 的处理 default: // ❌ 错误:类型 Square 不能赋值给类型 never const _exhaustiveCheck: never = shape return _exhaustiveCheck }}交叉类型#
交叉类型使用 & 表示一个值必须同时具有多种类型的特性:
interface Person { name: string age: number}
interface Employee { employeeId: string department: string}
// 交叉类型:同时具有 Person 和 Employee 的所有属性type Worker = Person & Employee
const worker: Worker = { name: '张三', age: 30, employeeId: 'E001', department: '技术部',}交叉类型的合并#
// 基础类型交叉(通常得到 never)type T1 = string & number // never(不存在既是字符串又是数字的值)
// 对象类型交叉(合并属性)type A = { a: string }type B = { b: number }type AB = A & B // { a: string; b: number }
// 相同属性名的交叉type X = { prop: string }type Y = { prop: number }type XY = X & Y // { prop: never }(string & number = never)
// 相同属性名但类型兼容时可以交叉type P = { name: string }type Q = { name: 'hello' } // 字面量类型type PQ = P & Q // { name: "hello" }实际应用:混入模式#
// 时间戳混入interface Timestamped { createdAt: Date updatedAt: Date}
// ID 混入interface Identifiable { id: string}
// 基础用户interface BaseUser { name: string email: string}
// 完整用户 = 基础用户 + 混入type User = BaseUser & Identifiable & Timestamped
const user: User = { id: 'u123', name: '张三', email: 'zhangsan@example.com', createdAt: new Date(), updatedAt: new Date(),}函数交叉类型#
type LogFn = (message: string) => voidtype ErrorFn = (error: Error) => void
// 函数交叉 = 函数重载type Logger = LogFn & ErrorFn
const logger: Logger = (input: string | Error) => { if (typeof input === 'string') { console.log(input) } else { console.error(input.message) }}
logger('info message')logger(new Error('error message'))联合类型 vs 交叉类型#
// 联合类型:或 (A | B)// 值可以是 A 或 B 其中之一type StringOrNumber = string | numberlet a: StringOrNumber = 'hello' // ✅let b: StringOrNumber = 42 // ✅
// 交叉类型:且 (A & B)// 值必须同时满足 A 和 Btype Named = { name: string }type Aged = { age: number }type Person = Named & Aged
const person: Person = { name: '张三', // 必须有 age: 25, // 必须有}🤔 直觉上的误解:
// 联合类型接受的值范围更广(或)type T1 = { a: string } | { b: number }const v1: T1 = { a: 'hello' } // ✅ 只需满足其一const v2: T1 = { b: 42 } // ✅
// 交叉类型接受的值范围更窄(且)type T2 = { a: string } & { b: number }const v3: T2 = { a: 'hello', b: 42 } // ✅ 必须同时满足// const v4: T2 = { a: 'hello' }; // ❌ 缺少 b实际应用场景#
API 响应处理#
// API 响应可能是成功或失败interface SuccessResponse<T> { status: 'success' data: T}
interface ErrorResponse { status: 'error' error: string code: number}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse
function handleResponse<T>(response: ApiResponse<T>) { if (response.status === 'success') { // response: SuccessResponse<T> console.log('Data:', response.data) } else { // response: ErrorResponse console.error(`Error ${response.code}: ${response.error}`) }}Redux Action 模式#
interface AddTodoAction { type: 'ADD_TODO' payload: { text: string }}
interface RemoveTodoAction { type: 'REMOVE_TODO' payload: { id: number }}
interface ToggleTodoAction { type: 'TOGGLE_TODO' payload: { id: number }}
type TodoAction = AddTodoAction | RemoveTodoAction | ToggleTodoAction
function todoReducer(state: Todo[], action: TodoAction): Todo[] { switch (action.type) { case 'ADD_TODO': return [ ...state, { id: Date.now(), text: action.payload.text, done: false }, ] case 'REMOVE_TODO': return state.filter((todo) => todo.id !== action.payload.id) case 'TOGGLE_TODO': return state.map((todo) => todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo ) }}组合配置#
// 基础配置interface BaseConfig { name: string version: string}
// 开发配置interface DevConfig { debug: true hotReload: boolean}
// 生产配置interface ProdConfig { debug: false minify: boolean sourceMaps: boolean}
// 完整配置 = 基础 + (开发 | 生产)type AppConfig = BaseConfig & (DevConfig | ProdConfig)
const devConfig: AppConfig = { name: 'MyApp', version: '1.0.0', debug: true, hotReload: true,}
const prodConfig: AppConfig = { name: 'MyApp', version: '1.0.0', debug: false, minify: true, sourceMaps: false,}常见问题#
🙋 联合类型数组和数组的联合有什么区别?#
// (string | number)[] - 数组,元素可以是 string 或 numberconst arr1: (string | number)[] = [1, 'hello', 2, 'world']
// string[] | number[] - 要么全是 string,要么全是 numberconst arr2: string[] | number[] = [1, 2, 3] // ✅const arr3: string[] | number[] = ['a', 'b', 'c'] // ✅// const arr4: string[] | number[] = [1, 'a']; // ❌ 混合不行🙋 如何从联合类型中提取特定类型?#
type Animal = { type: 'dog'; bark(): void } | { type: 'cat'; meow(): void }
// 使用 Extract 提取type Dog = Extract<Animal, { type: 'dog' }>// { type: 'dog'; bark(): void }
// 使用 Exclude 排除type NotDog = Exclude<Animal, { type: 'dog' }>// { type: 'cat'; meow(): void }🙋 交叉类型遇到冲突怎么办?#
// 属性类型冲突会得到 nevertype A = { x: string }type B = { x: number }type C = A & B // { x: never }
// 解决方案:使用 Omit 排除冲突属性type D = Omit<A, 'x'> & B // { x: number }总结#
| 类型 | 语法 | 含义 | 适用场景 |
|---|---|---|---|
| 联合类型 | A | B | 值是 A 或 B 之一 | 多种可能的输入、状态机 |
| 交叉类型 | A & B | 值同时具有 A 和 B 的特性 | 混入、组合对象 |
| 可辨识联合 | 带共同字面量属性的联合 | 可靠的类型收窄 | Redux Action、API 响应 |
下一篇我们将学习字面量类型与类型别名,深入理解 TypeScript 的类型定义方式。