reactjs - TypeScript 用於條件屬性的工具類型(基於類型中其他屬性輸入的值)
我經常需要定義一個類型物件,其中一個屬性鍵僅在類型的另一個屬性/屬性為某個值時才被接受。
一個簡單的例子(在 React 的背景下,但應該適用於任何情況)是我需要一個 Button 類型的物件,它接受以下屬性:
type Button = {
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
hasFancyOutline?: boolean;
}
現在,實際上我不希望該類型在 appearance 不是 outline 且 isDisabled 為 false 時接受 hasFancyOutline。
正確的做法是這樣:
type SharedButtonProps = {
size: 'small' | 'large';
}
type NonOutlineButtonProps = SharedButtonProps & {
appearance: solid' | 'minimal';
isDisabled?: boolean;
}
type OutlineButtonProps = SharedButtonProps & {
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
}
type Button = NonOutlineButtonProps | OutlineButtonProps
我想寫一個名為 ConditionalProps 的速記工具類型,它可以智能地為我做到這一點。類似這樣的東西:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
},
{
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
}
>
我正在用偽代碼思考,它大概會這樣運作:
type ConditionalProps<BaseProps, ConditionalProps> = {
// 1. Find keys with the same name in BaseProps & ConditionalProps. Optional and non-optional types such as `isDisabled?` and `isDisabled` need to be matched.
type MatchingProps = Match<BaseProps, ConditionalProps> // { appearance: 'solid' | 'outline' | 'minimal', isDisabled?: boolean }
type SharedProps = Omit<BaseProps, MatchingProps> // { size: 'small' | 'large' }
// 2. Find what's the values of the props if they don't match the condition, e.g. 'appearance' would be either 'solid' or 'minimal'
type FailConditionProps = RemainingValues<MatchingProps, ConditionalProps> // { appearance: 'solid' | 'minimal'; isDisabled?: boolean; }
// 3. Assemble
type FailConditionPlusSharedProps = SharedProps & FailConditionProps
type PassConditionPlusSharedProps = SharedProps & ConditionalProps
return FailConditionPlusSharedProps | PassConditionPlusSharedProps
}
編輯
下面 Titian 的回答是這個問題的確切解決方案。但我在想是否有辦法重寫 ConditionalProps 使其更好。
我發現自己寫了很多依據所給值而有條件的類型。
所以例如,
type Button = {
size: 'small' | 'large';
isReallyBig?: boolean;
appearance: 'solid' | 'outline' | 'minimal';
hasFancyOutline?: boolean;
outlineBackgroundColor: string;
isDisabled?: boolean;
isLoading?: boolean;
}
說我想做:
- 只有當
size='large'時,才接受isReallyBig? hasFancyOutline?與outlineBackgroundColor只有在appearance=‘outline’且isDisabled=false時才被接受isLoading只有在isDisabled=true時才能為true。
如果我想重寫 ConditionalProps 以清楚地定義這個類型,我該怎麼做呢?我在想實作可能會像是這樣:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
outlineBackgroundColor: string;
isDisabled?: boolean;
},
[
[
{ size: 'large' },
{ isReallyBig?: boolean }
], [
{ appearance: 'outline', isDisabled: false },
{ hasFancyOutline?: boolean }
], [
{ isDisabled: true },
{ isLoading?: boolean }
]
]
>
像這樣的事情可以實現,還是有更好的方法來處理這種情況?
答案#1
在實現這個的過程中,我遇到的問題是,不明顯為什麼只有 appearance 的值應該從常規情況中移除。isDisabled 是 true | false 的聯合類型,因此,從常規情況中移除所有值會導致在默認情況下 isDisabled 的 false 被移除。這可能不是期望的行為。
如果我們添加一個屬性來說明判別式是什麼,我們就可以建立你想要的類型
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
}, 'appearance',
{
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
}
>
type RemoveCommonValues<T, TOmit> = {
[P in keyof T]: TOmit extends Record<P, infer U> ? Exclude<T[P], U> : T[P]
}
type Omit<T, K extends PropertyKey> = Pick<T, Exclude<keyof T, K>> // not needed in 3.5
type Id<T> = {} & { [P in keyof T]: T[P] } // flatens out the types to make them more readable can be removed
type ConditionalProps<T, TKey extends keyof TCase, TCase extends Partial<T>> =
Id<Omit<T, keyof TCase> & TCase>
| Id<RemoveCommonValues<T, Pick<TCase, TKey>>>
RemoveCommonValues 會遍歷共同屬性,如果這些屬性在 TOmit 上被定義,則會從共同值中移除那裡定義的值。要獲取 TOmit 情況下定義的屬性,我們需要獲取共同屬性 (Omit<T, keyof TOmit>) 並將它們與 TOmit 交集。
測試它:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
}, 'appearance',
{
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
}
>
// same as
type Button = {
size: "small" | "large";
appearance: "outline";
isDisabled: false;
hasFancyOutline?: boolean | undefined;
} | {
size: "small" | "large";
appearance: "solid" | "minimal";
isDisabled?: boolean | undefined;
}
我們可以傳入多個案例:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
}, 'appearance' ,{
appearance: 'outline';
isDisabled: false;
hasFancyOutline?: boolean;
} | {
appearance: 'minimal';
isDisabled: false;
useReadableFont?: boolean;
}
>
// same as
type Button = {
size: "small" | "large";
appearance: "outline";
isDisabled: false;
hasFancyOutline?: boolean | undefined;
} | {
size: "small" | "large";
appearance: "minimal";
isDisabled: false;
useReadableFont?: boolean | undefined;
} | {
size: "small" | "large";
appearance: "solid";
isDisabled?: boolean | undefined;
}
如果我們想要有更多判別鍵,但這是如何運作的就不太清楚,因為這不太容易組合。你可以傳入多個鍵,但你必須確保你傳入的案例涵蓋所有可能的組合,因為任何值都會從結果中被移除:
type Button = ConditionalProps<
{
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
}, 'appearance' | 'size' ,{
appearance: 'outline';
size: 'small'
isDisabled: false;
hasFancyOutline?: boolean;
} | {
appearance: 'minimal';
size: 'small'
isDisabled: false;
hasFancyOutline?: boolean;
}
>
// same as
type Button = {
appearance: "outline";
size: "small";
isDisabled: false;
hasFancyOutline?: boolean | undefined;
} | {
appearance: "minimal";
size: "small";
isDisabled: false;
hasFancyOutline?: boolean | undefined;
} | {
size: "large";
appearance: "solid";
isDisabled?: boolean | undefined;
}
不可能有 minimal large 按鈕。
