JavaScript - TypeScript 判別聯合
我正在嘗試了解如何強制執行區分聯合類型。我有一個類型定義,技術上 TypeScript 會接受,但對我來說並沒有如預期般運作。
注意:我之前忘了提到,我需要這些類型定義是為了一個函數型元件。
有一種官方方法可以定義區別聯合類型。然而,在 IDE 中,它會拋出以下錯誤:Property 'startDate' does not exist on type 'MyComponentProps'.ts(2339)
TypeScript 接受的範例,但在 IDE 中顯示錯誤
import { FC } from "react";
interface BaseProps {
name: string;
age: number;
}
type IsEligibleProps = {
eligible: true;
startDate: string;
};
type NotEligibleProps = {
eligible: false;
reason: string;
};
type MyComponentProps = BaseProps & (IsEligibleProps | NotEligibleProps);
const MyComponent: FC<MyComponentProps> = ({
name,
age,
eligible = false,
startDate,
reason
}) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Eligible: {eligible ? "true" : "false"}</p>
<p>Response: {eligible ? startDate : reason}</p>
</div>
);
};
export default MyComponent;
我已經設法找到一個低效的解決方案,這個方案要求我為所有可能的變體重新聲明所有的屬性,但將我不需要的屬性設置為 null/never。
TypeScript 可接受的範例,且不會拋出任何錯誤
import { FC } from "react";
interface BaseProps {
name: string;
age: number;
}
type EligibleProps =
| {
eligible: true;
startDate: string;
reason?: never;
}
| {
eligible?: false;
reason: string;
startDate?: never;
};
type MyComponentProps = BaseProps & EligibleProps;
const MyComponent: FC<MyComponentProps> = ({
name,
age,
eligible = false,
startDate,
reason
}) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Eligible: {eligible ? "true" : "false"}</p>
<p>Response: {eligible ? startDate : reason}</p>
</div>
);
};
export default MyComponent;
我的問題是——有沒有辦法使用原始的區分聯合定義,並讓函數式組件也能讀取它,並在解構的 props 列表上確定相應的適當 props?
錯誤程式碼的連結
鏈接到可運行但效率低下的代碼
答案#1
目前,你只能將聯合類型解構成物件,如果該物件的屬性在聯合的每個成員中都存在。通常這是一個合理的限制,但當你有一個可區分的聯合類型時,它會解除這個限制。Microsoft/TypeScript#46318 有一個開放的功能請求標記為「等待更多反饋」,所以如果你希望這個功能實現,給該議題點個 👍 並可能描述一下你的使用案例為何具有說服力也無妨。(可能獲得單一的額外投票幫助不大……但也不會有壞處。)
在那之前,除非它被實現,否則你需要想辦法迴避。一種解決方法就是放棄拆解相關屬性,而以「正常」的方式訪問它們:
const MyComponent: FC<MyComponentProps> = ({
name,
age,
...rest
}) => {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Eligible: {rest.eligible ? "true" : "false"}</p>
<p>Response: {rest.eligible ? rest.startDate : rest.reason}</p>
</div>
);
};
這裡的 rest 只是對應於聯合辨識符及其屬性的物件部分。然後,不再引用 eligible、startDate 或 reason,而是訪問 rest.eligible、rest.startDate 和 rest.reason,這樣就可以了。
另一種變通方法是增強區分聯合型別,使每個成員都提及所有其他成員的所有屬性,並通過將「交叉屬性」設為不可能的 never 型別的可選屬性來明確禁止它們。 這將確保當 eligible 為 true 時,reason 應被認為是 undefined。 暫且沒有更好的術語,我稱這種方法為對聯合型別進行「排他化」(exclusifying)。
事實上,這就是你所說的「低效率」的技術。幸運的是,你不需要手動做這件事。相反地,你可以寫一個實用型態函數來自動排他聯集。它可能看起來像這樣:
type Exclusify<
T extends object,
K extends PropertyKey = AllKeys<T>
> =
T extends unknown ? (
{ [P in K]?: P extends keyof T ? unknown : never } & T
) extends infer U ? { [P in keyof U]: U[P] } : never : never;
type AllKeys<T> = T extends unknown ? keyof T : never;
本質上,這是一種類型的分佈式條件,將 T 分解為其聯合成員(這就是 T extends unknown ? ⋯ : never 部分),並對每個成員,與任意聯合成員的所有鍵交集成一個物件類型,對於當前成員中不存在的任何鍵,類型為 never。
請注意,「來自任何聯合成員的所有鍵」需要在我們將 T 拆分成聯合成員之前計算;我通過引入 K 並給它一個默認的泛型類型參數 AllKeys<T> 來做到這一點,這是一個獨立的分配條件類型。
交集有時看起來也很醜,因此我已經使用在“如何查看 TypeScript 類型的完整展開合約?”中討論的技術將它們合併成單一物件類型。
讓我們來測試它:
type MyComponentProps = Exclusify<BaseProps & (IsEligibleProps | NotEligibleProps)>;
/* type MyComponentProps = {
name: string;
age: number;
eligible: true;
startDate: string;
reason?: undefined;
} | {
name: string;
age: number;
eligible?: false | undefined;
startDate?: undefined;
reason: string;
} */
看起來不錯;那是你之前使用的同一類型(所以你知道它可以用),但現在你可以添加任意多的聯合成員,它應該會自動維護。
程式碼的 Playground 連結
答案#2
我認為你的第一個程式碼完成了這個任務。
介面 BaseProps { name: string; age: number; }
type IsEligibleProps = {
isEligible: true;
startDate: string;
};
type NotEligibleProps = {
isEligible: false;
reason: string;
};
export type EligibleProps = BaseProps & (IsEligibleProps | NotEligibleProps);
const myArr: EligibleProps[] = [{
name:"Spongebob",
age:42,
isEligible: true,
startDate:"1/1/21"
},{
name:"Spongebob",
age:42,
isEligible:false,
reason:"Test"
}]
更新:
我想提供最靈活的解決問題選項。我們真的需要一個具有所有屬性的介面。我決定將其值用作caption和val。這樣,我們就可以創建一個具有所需排序和標題設置的對象。
介面:
...
interface Mixed extends BaseProps {
eligible: boolean;
startDate: string;
reason: string;
}
export type MyComponentMixProps = {
[Property in keyof Mixed as string]: {
caption: string,
val: string
}
}
組件:
import { MyComponentProps, MyComponentMixProps } from "./interface";
const MyComponent: FC<MyComponentProps> = props => {
const getOrdredProps = () => {
// set an order and captions
const order: MyComponentMixProps = {
name: { caption: 'Name', val: ''},
age: { caption: 'Age', val: ''},
eligible: { caption: 'Eligable', val: ''},
startDate: { caption: 'Start date', val: ''},
reason:{ caption: 'Reason', val: ''},
}
// assign eligible=false (when it isn't passed)
const eligibleProps = props.eligible
? props
: Object.assign({eligible: false}, props)
// set an order and stringify values
for(const [k, v] of Object.entries(eligibleProps)){
order[k].val = v.toString()
}
// filter empty values
const result = Object.fromEntries(Object.entries(order)
.filter(([_, {val}]) => val !== ''))
return result
}
return (
<div>
{ Object.entries(getOrdredProps())
.map(([key, {caption, val}]) => (
<p key={key}>{caption}: {val}</p>
))
}
</div>
);
};
連結到 codesandbox
