TypeScript 聯合與交集型別 (Union and Intersection Types)
聯合型別和交集型別是 TypeScript 中用來組合型別的兩種重要方式。它們讓我們可以更靈活地定義型別。
聯合型別 (Union Types)
聯合型別表示一個值可以是多種型別之一,使用 | 符號連接:
type StringOrNumber = string | number;
let value: StringOrNumber;
value = 'hello'; // OK
value = 42; // OK
// value = true; // 錯誤:boolean 不在聯合型別中
常見用途
處理多種輸入型別
function formatValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
console.log(formatValue('hello')); // HELLO
console.log(formatValue(3.14159)); // 3.14
可空型別
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type Maybe<T> = T | null | undefined;
let name: Nullable<string> = '小明';
name = null; // OK
function greet(name: Optional<string>): void {
if (name) {
console.log(`Hello, ${name}!`);
} else {
console.log('Hello, stranger!');
}
}
聯合型別的屬性存取
只能存取所有型別共有的屬性:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getAnimal(): Bird | Fish {
// ...
}
let pet = getAnimal();
pet.layEggs(); // OK,兩者都有
// pet.fly(); // 錯誤,Fish 沒有 fly 方法
型別收窄 (Type Narrowing)
使用型別檢查來收窄聯合型別:
function process(value: string | number | boolean): void {
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');
}
}
可辨識聯合 (Discriminated Unions)
使用共同的字面量屬性來區分聯合型別中的成員:
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;
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;
}
}
console.log(getArea({ kind: 'circle', radius: 5 })); // 78.54...
console.log(getArea({ kind: 'rectangle', width: 4, height: 5 })); // 20
窮盡檢查 (Exhaustiveness Checking)
使用 never 確保處理了所有情況:
function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x);
}
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:
// 如果遺漏了某個 case,這裡會報錯
return assertNever(shape);
}
}
交集型別 (Intersection Types)
交集型別將多個型別合併為一個,使用 & 符號:
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
let person: Person = {
name: '小明',
age: 25,
};
交集型別必須同時滿足所有型別的要求。
合併介面
interface Printable {
print(): void;
}
interface Loggable {
log(message: string): void;
}
type PrintableAndLoggable = Printable & Loggable;
class Document implements PrintableAndLoggable {
print(): void {
console.log('Printing...');
}
log(message: string): void {
console.log(message);
}
}
擴展物件型別
type BaseUser = {
id: number;
name: string;
};
type UserWithEmail = BaseUser & {
email: string;
};
type AdminUser = UserWithEmail & {
role: 'admin';
permissions: string[];
};
let admin: AdminUser = {
id: 1,
name: '管理員',
email: 'admin@example.com',
role: 'admin',
permissions: ['read', 'write', 'delete'],
};
聯合型別 vs 交集型別
| 特性 | 聯合型別 (|) | 交集型別 (&) |
|---|---|---|
| 語意 | 「或」- 其中之一 | 「且」- 同時都是 |
| 屬性存取 | 只能存取共有屬性 | 可存取所有屬性 |
| 物件合併 | 不合併 | 合併所有屬性 |
// 聯合型別:A 或 B
type AorB = { a: string } | { b: number };
let x: AorB = { a: 'hello' }; // OK
let y: AorB = { b: 42 }; // OK
// 交集型別:A 且 B
type AandB = { a: string } & { b: number };
let z: AandB = { a: 'hello', b: 42 }; // 必須同時有 a 和 b
原始型別的交集
原始型別的交集通常會變成 never:
type StringAndNumber = string & number; // never
// 沒有值可以同時是 string 和 number
但對於物件型別,交集會合併屬性:
type A = { shared: string; a: number };
type B = { shared: string; b: boolean };
type AB = A & B;
// { shared: string; a: number; b: boolean }
屬性衝突
當交集型別中有衝突的屬性時:
type A = { value: string };
type B = { value: number };
type AB = A & B;
// { value: string & number } = { value: never }
// AB 實際上無法使用,因為 value 必須同時是 string 和 number
但如果屬性是物件型別,會遞迴合併:
type A = { info: { a: string } };
type B = { info: { b: number } };
type AB = A & B;
// { info: { a: string; b: number } }
let ab: AB = {
info: {
a: 'hello',
b: 42,
},
};
實用範例
API 回應處理
type SuccessResponse<T> = {
success: true;
data: T;
};
type ErrorResponse = {
success: false;
error: string;
code: number;
};
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
async function fetchUser(id: number): Promise<ApiResponse<User>> {
try {
const user = await api.get(`/users/${id}`);
return { success: true, data: user };
} catch (e) {
return { success: false, error: 'User not found', code: 404 };
}
}
async function handleUserFetch() {
const response = await fetchUser(1);
if (response.success) {
// response.data 可用
console.log(response.data.name);
} else {
// response.error 可用
console.error(response.error);
}
}
狀態機
type IdleState = { status: 'idle' };
type LoadingState = { status: 'loading' };
type SuccessState<T> = { status: 'success'; data: T };
type ErrorState = { status: 'error'; error: string };
type AsyncState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return '等待中';
case 'loading':
return '載入中...';
case 'success':
return `成功: ${JSON.stringify(state.data)}`;
case 'error':
return `錯誤: ${state.error}`;
}
}
混入模式 (Mixin Pattern)
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = new Date();
};
}
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActive = false;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
};
}
class User {
constructor(public name: string) {}
}
// 組合多個 mixin
const TimestampedActivatableUser = Timestamped(Activatable(User));
const user = new TimestampedActivatableUser('小明');