先看範例:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
function area(s: Shape) {
if (s.kind === "circle") {
return Math.PI * s.radius * s.radius;
} else if (s.kind === "square") {
return s.x * s.x;
} else {
return (s.x * s.y) / 2;
}
}
在這個範例中,Shape
是一個 Union Type。每個型別都有辨識屬性 kind
,TypeScript 會根據這個屬性來區分型別。例如,當 kind
是 "circle"
時,TypeScript 可以推斷 s
一定有 radius
屬性,而不會有其他不相關的屬性。
透過 if
條件,TypeScript 可以縮小型別範圍,當判斷條件 s.kind === "circle"
為真時,TypeScript 會將 s
的型別縮小到 circle
,這樣就可以安全地訪問 radius
屬性。
當然除了 if
,switch
也有同樣的效果。
Type Guards : 是指在透過特定的邏輯判斷來縮小變數的型別範圍,有時也會搭配 typeof
, instanceof
, in
等關鍵字使用。
Narrowing : 是指將變數的型別從更廣泛的範圍縮小到具體型別的過程,常搭配 Type Guards 使用。
Discriminated Unions 適合在處理多種可能的狀態或多個不同類型的情境時使用。特別是當我們有多個型別之間有部份差異,但還是共享某些共同屬性時。常用於下列情境:
這邊我寫一個 UserCard 的 Component,總共分成三種情況:
type UserCardProps = {
name: string;
role: "user" | "admin" | "guest";
permissions?: string[]; // 只有 admin 才有這個屬性,但這裡是可選的
email?: string; // 只有 user 才有 email,但這裡也是可選的
};
export default function UserCard({ props }: { props: UserCardProps }) {
const { name, role, permissions, email } = props;
if (role === "admin") {
return (
<div>
<h2>{name} (Admin)</h2>
<p>Permissions: {permissions?.join(", ")}</p>
</div>
);
}
if (role === "user") {
return (
<div>
<h2>{name} (User)</h2>
<p>Email: {email}</p>
</div>
);
}
return (
<div>
<h2>{name} (Guest)</h2>
</div>
);
}
這個範例的問題:
role
是 'guest'
,有可能會意外傳入 permissions
或 email
。permissions?.join(", ")
或 email
的時候,如果這些屬性沒有被傳入,結果將會是 undefined,這可能會導致顯示錯誤和其他問題。type AdminProps = {
role: "admin";
permissions: string[];
};
type UserProps = {
role: "user";
email: string;
};
type GuestProps = {
role: "guest";
};
type UserCardProps = {
name: string;
} & (AdminProps | UserProps | GuestProps);
export default function UserCard({ props }: { props: UserCardProps }) {
const { name, role } = props;
// 如果在這邊直接取 permissions 或 email 會出現錯誤,因為 TypeScript 知道這些屬性不是通用的
if (role === "admin") {
const { permissions } = props;
// TypeScript 確保當 role 是 "admin" 時,permissions 一定存在
return (
<div>
<h2>{name} (Admin)</h2>
<p>Permissions: {permissions.join(", ")}</p>
</div>
);
}
if (role === "user") {
const { email } = props;
// 這邊也是一樣,當 role 是 "user" 時,email 一定存在
return (
<div>
<h2>{name} (User)</h2>
<p>Email: {email}</p>
</div>
);
}
return (
<div>
<h2>{name} (Guest)</h2>
</div>
);
}
參考資料:
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
https://www.typescriptlang.org/docs/handbook/2/narrowing.html
https://www.totaltypescript.com/discriminated-unions-are-a-devs-best-friend