跳至主要内容

[typescript] React Component Discriminated Unions (可辨識複合型別)

影片介紹

介紹​

今天要介紹如何使用 typescript 的 discriminated unions 來建立更彈性化的 component。

問題點​

直接看範例程式碼,在 App 內我們根據 goals 的長度來去決定 InfoBox 這個 component 要帶入的參數(props)是什麼。

App.tsx
type CourseGoalListProps = {
goals: CGoal[];
onDeleteGoal: (id: number) => void;
};

const App = ({ goals, onDeleteGoal }: CourseGoalListProps) => {
if (goals.length === 0) {
return <InfoBox mode="hint">You have no course goals yet!</InfoBox>;
}

let warningBox: ReactNode;

if (goals.length >= 4) {
warningBox = (
<InfoBox mode="warning" severity="high">
You're collection a lot of goals!
</InfoBox>
);
}

return <>...</>;
};

而 InfoBox 的程式碼如下,我們可以看到 InfoBox 必須傳入 mode、severity、children 這三個 props,只要少一個就會出現錯誤。

所以現在 App 的第 8 行程式碼會出現 Property 'severity' is missing in type '{ children: string; mode: "hint"; }' but required in type 'InfoBoxProps'. 這個錯誤,因為我們少傳入了 severity 這個 prop。

但我們希望在 goals.length === 0 也就是 mode === "hint" 的時候,可以不用傳入 severity。

InfoBox.tsx
type InfoBoxProps = {
mode: "warning" | "hint";
severity: "low" | "medium" | "high";
children: ReactNode;
};

const InfoBox = (props: InfoBoxProps) => {
const { children, mode } = props;

if (mode === "hint") {
return (
<aside className="infobox infobox-hint">
<p>{children}</p>
</aside>
);
}

const { severity } = props;

return (
<aside className={`infobox infobox-warning warning--${severity}`}>
<h2>Warning</h2>
<p>{children}</p>
</aside>
);
};

解決方法 (1) Optional​

這時候你可能會想說,把 severity 變成 optional(?) 的就好了吧?

InfoBox.tsx
type InfoBoxProps = {
mode: "warning" | "hint";
severity?: "low" | "medium" | "high";
children: ReactNode;
};

const InfoBox = (props: InfoBoxProps) => {
const { children, mode } = props;

if (mode === "hint") {
return (
<aside className="infobox infobox-hint">
<p>{children}</p>
</aside>
);
}

const { severity } = props;

return (
<aside className={`infobox infobox-warning warning--${severity}`}>
<h2>Warning</h2>
<p>{children}</p>
</aside>
);
};

這樣做的確可行,但現在在 goals.length >= 4 也就是 mode==="warning" 的條件下去渲染 InfoBox 會有一個問題發生,就是即使我們不傳入 severity 也不會出現任何錯誤,這顯然不是我們想要的。

App.tsx
...

const App = ({ goals, onDeleteGoal }: CourseGoalListProps) => {
if (goals.length === 0) {
return <InfoBox mode="hint">You have no course goals yet!</InfoBox>;
}

let warningBox: ReactNode;

if (goals.length >= 4) {
warningBox = (
// 把 severity="high" 拿掉也不會出現錯誤
<InfoBox mode="warning">
You're collection a lot of goals!
</InfoBox>
);
}

return <>...</>;
};

解決方法 (2) Discriminated Unions​

較佳的解決方式是使用 Discriminated Unions,程式碼如下,可以看到我們將 mode 為 hint 及 mode 為 warning 的 type 拆開來了,並且宣告 InfoBoxProps 為 Discriminated Unions。

InfoBox.tsx
type HintBoxProps = {
mode: "hint";
children: ReactNode;
};

type WarningInfoBoxProps = {
mode: "warning";
severity: "low" | "medium" | "high";
children: ReactNode;
};

type InfoBoxProps = HintBoxProps | WarningInfoBoxProps;

const InfoBox = (props: InfoBoxProps) => {
const { children, mode } = props;

if (mode === "hint") {
return (
<aside className="infobox infobox-hint">
<p>{children}</p>
</aside>
);
}

const { severity } = props;

return (
<aside className={`infobox infobox-warning warning--${severity}`}>
<h2>Warning</h2>
<p>{children}</p>
</aside>
);
};

這時候如果我們在 mode = "warning" 的時候,將 severity prop 拿掉,typescript 會出現 Property 'severity' is missing in type '{ children: string; mode: "warning"; }' but required in type 'WarningInfoBoxProps'. 錯誤,我們就知道必須傳入 severity prop。

App.tsx
const App = ({ goals, onDeleteGoal }: CourseGoalListProps) => {
if (goals.length === 0) {
return <InfoBox mode="hint">You have no course goals yet!</InfoBox>;
}

let warningBox: ReactNode;

if (goals.length >= 4) {
warningBox = (
// 把 severity="high" 拿掉會出現錯誤
<InfoBox mode="warning">You're collection a lot of goals!</InfoBox>
);
}

return <>...</>;
};

而使用 Discriminated Unions 的好處還有一點,就是當我們在 mode === "hint" 的情況下,如果去解構 severity,則會出現 Property 'severity' does not exist on type 'HintBoxProps'. 錯誤。

因為 typescript 知道 severity 並不存在 HintBoxProps 內。

Image

Image

如果改成 warning 的話,就不會出現這個錯誤,因為 severity 有存在 WarningInfoBoxProps 內。

Image

Image

參考資料​

udemy 課程 React & TypeScript - The Practical Guide Academind by Maximilian Schwarzmüller, Maximilian Schwarzmüller