BioCrossCoder
2024-02-08
联合类型(`A | B`)和交叉类型(`A & B`)看似增加了类型复杂度,但在很多场景下,它们能**精准描述数据的“或”“且”关系**,反而会让类型逻辑更清晰、代码更安全。避开它们可能导致用 `any` 替代或重复定义类型,反而增加维护成本。下面结合具体场景举例说明: ### 一、联合类型(`A | B`):处理“或”关系的场景 联合类型的核心是“类型可以是多个选项中的一个”,适合描述**存在多种可能形态的数据或状态**。 #### 1. 状态管理:同一变量的不同状态(“成功/失败/加载中”模式) 在前端状态管理中(如接口请求),一个变量往往有多种状态,且不同状态携带的数据不同。用联合类型可以清晰区分: ```typescript // 定义三种状态的类型 type LoadingState = { status: 'loading' }; type SuccessState = { status: 'success'; data: string }; type ErrorState = { status: 'error'; error: Error }; // 联合类型:状态只能是三者之一 type RequestState = LoadingState | SuccessState | ErrorState; // 使用时,TypeScript 会根据 status 自动推断具体类型(类型收窄) function handleState(state: RequestState) { switch (state.status) { case 'loading': console.log('加载中...'); break; case 'success': // 这里能直接访问 data,因为 TypeScript 知道是 SuccessState console.log('成功:', state.data); break; case 'error': // 这里能直接访问 error,因为 TypeScript 知道是 ErrorState console.log('失败:', state.error.message); break; } } ``` **为什么适合?** 如果不用联合类型,可能需要用一个包含所有可能属性的类型(`{ status: string; data?: string; error?: Error }`),但这样无法保证“成功时一定有data”“失败时一定有error”,容易出现 `state.data` 为 `undefined` 的运行时错误。联合类型通过“互斥的标志(如status)”实现了类型安全。 #### 2. 处理多种可能的输入/参数 当函数参数可能是多种类型(但需要统一处理)时,联合类型可以避免用 `any`,同时保留类型提示: ```typescript // 支持“用户ID(数字)”或“用户对象”作为参数 type User = { id: number; name: string }; type UserInput = number | User; // 根据输入类型获取用户ID function getUserId(input: UserInput): number { if (typeof input === 'number') { return input; // 输入是数字时直接返回 } else { return input.id; // 输入是对象时返回id(TypeScript 自动推断) } } ``` **为什么适合?** 如果不用联合类型,要么写两个重载函数(繁琐),要么用 `any`(丢失类型检查)。联合类型既灵活又安全。 #### 3. 描述“可选的扩展属性” 当一个基础类型可能有多种扩展形态(但不是所有扩展都必须存在)时,联合类型可以避免属性冗余: ```typescript // 基础商品类型 type BaseProduct = { id: number; name: string; price: number }; // 扩展类型:实体商品有重量,虚拟商品有有效期 type PhysicalProduct = BaseProduct & { type: 'physical'; weight: number }; type VirtualProduct = BaseProduct & { type: 'virtual'; expiry: string }; // 联合类型:商品要么是实体,要么是虚拟 type Product = PhysicalProduct | VirtualProduct; // 使用时,根据 type 区分处理 function handleProduct(product: Product) { if (product.type === 'physical') { console.log('重量:', product.weight); // 实体商品必有的属性 } else { console.log('有效期:', product.expiry); // 虚拟商品必有的属性 } } ``` **为什么适合?** 如果用一个包含所有扩展属性的类型(`{ type: string; weight?: number; expiry?: string }`),则无法保证“实体商品一定有weight”,可能导致访问 `product.weight` 时出现 `undefined`。联合类型通过“类型标志(type)”确保了属性的存在性。 ### 二、交叉类型(`A & B`):处理“且”关系的场景 交叉类型的核心是“类型同时具备多个类型的特征”,适合描述**需要合并多个独立类型的属性或能力**的场景。 #### 1. 合并多个配置对象 在配置场景中,经常需要将“基础配置”和“扩展配置”合并为一个完整配置,交叉类型可以自然实现: ```typescript // 基础配置:必选的核心属性 type BaseConfig = { apiUrl: string; timeout: number; }; // 扩展配置:可选的附加功能 type ExtendConfig = { retry?: number; // 重试次数 log?: boolean; // 是否打印日志 }; // 交叉类型:完整配置 = 基础配置 + 扩展配置 type FullConfig = BaseConfig & ExtendConfig; // 使用时,必须包含基础配置,可选扩展配置 const config: FullConfig = { apiUrl: 'https://api.example.com', timeout: 5000, retry: 3 // 可选的扩展属性 }; ``` **为什么适合?** 如果不用交叉类型,需要手动定义 `FullConfig` 并重复 `BaseConfig` 和 `ExtendConfig` 的属性,一旦基础配置修改,完整配置也要同步修改,容易出错。交叉类型实现了“配置的复用与合并”。 #### 2. 实现“混合类型”(同时具备多个类/接口的能力) 在面向对象中,一个对象可能需要同时拥有多个类的方法(类似多继承),交叉类型可以描述这种“混合”能力: ```typescript // 具备“行走”能力的类型 interface Walkable { walk: () => void; } // 具备“飞行”能力的类型 interface Flyable { fly: () => void; } // 交叉类型:同时具备行走和飞行能力 type Bird = Walkable & Flyable; // 实现一个鸟的对象 const bird: Bird = { walk: () => console.log('用脚走'), fly: () => console.log('用翅膀飞') }; ``` **为什么适合?** 如果不用交叉类型,需要定义一个新接口 `Bird` 并重复 `Walkable` 和 `Flyable` 的方法,违反“DRY(不重复)”原则。交叉类型直接组合已有接口,更简洁。 #### 3. 给类型动态添加属性(TypeScript 类型层面的“扩展”) 当需要基于已有类型添加新属性(但不修改原类型)时,交叉类型可以安全扩展: ```typescript // 原始用户类型 type User = { id: number; name: string }; // 扩展:给用户添加“权限”属性 type UserWithPermission = User & { permission: 'admin' | 'user' }; // 使用扩展后的类型 const admin: UserWithPermission = { id: 1, name: 'Alice', permission: 'admin' }; ``` **为什么适合?** 如果直接修改 `User` 类型添加 `permission`,可能影响其他使用 `User` 类型的地方(比如有些场景不需要权限属性)。交叉类型实现了“局部扩展”,不污染原类型。 ### 三、什么时候不适合用? - 当类型之间没有明确的“或”“且”关系时(比如强行把无关类型联合,如 `string | number | { a: 1 }`),会导致类型收窄困难,反而增加复杂度。 - 过度嵌套联合/交叉类型(如 `(A | B) & (C | D)`),可能让类型逻辑晦涩,此时可以拆分为中间类型简化。 ### 总结 联合类型和交叉类型的价值在于**精准描述数据的逻辑关系**: - 当数据“可能是A,也可能是B(但不能同时是两者)”时,用联合类型(`A | B`); - 当数据“必须同时具备A和B的特征”时,用交叉类型(`A & B`)。 合理使用时,它们不会增加复杂度,反而能通过 TypeScript 的类型检查减少错误,让代码的意图更清晰。
晓之蛇
2024-01-03
这是两种安全策略的问题,分为类型匹配和属性访问限制。
类型匹配:在你的代码中,变量 c2 被定义为 Course2 类型,这是 EnglishCourse | MathCourse 的联合类型。变量 c2 的结构 { name: 'a', desc: 'abc' } 符合 EnglishCourse 接口的定义(因为 EnglishCourse 有 name 和 desc 两个属性),所以 TypeScript 认为这是一个有效的赋值。
接下来,关于为什么你不能直接访问 c2.desc:
属性访问限制:尽管变量 c2 在定义时没有报错,但当你尝试访问 c2 的属性时,TypeScript 变得更加严格。在使用联合类型时,你只能访问所有类型共有的属性。由于 EnglishCourse 和 MathCourse 没有共有的属性(它们的 name 属性类型不同),直接访问 c2.desc 会导致错误。这是因为在不确定 c2 究竟是哪个类型的情况下,直接访问可能不存在的属性是不安全的。
因此,虽然在定义变量时 TypeScript 允许 c2 匹配 MathCourse 类型,但在后续访问属性时,由于 c2 可能是 EnglishCourse 或 MathCourse,直接访问特定于其中一种类型的属性(如 desc)会导致错误。要安全地访问这些属性,你需要使用类型守卫(如类型断言或 in 关键字检查)来确定 c2 的确切类型。
这两个方面并不矛盾,而是反映了 TypeScript 在不同场景下的类型安全策略:一方面,在赋值时允许某种程度的灵活性;另一方面,访问属性时则需要更严格的类型确定性。
weixin_慕桂英3508719
2023-12-06
啥啊?
慕少6168253
2023-11-02
是一种后端语言。