我经常需要定义一个类型对象,其中仅当该类型的另一个属性是某个特定值时才接受属性键。
一个简单的示例(在React的上下文中,但应该在任何情况下都适用)是我需要一个类型Button
对象,该对象接受以下属性:
type Button = {
size: 'small' | 'large';
appearance: 'solid' | 'outline' | 'minimal';
isDisabled?: boolean;
hasFancyOutline?: boolean;
}
现在,如果hasFancyOutline
不是appearance
并且outline
是isDisabled
,我实际上不希望类型接受false
。
正确的方法是:
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
}
下面的提香答案是确切的解决方案。但是我想知道是否有一种方法可以重写ConditionalProps
从而变得更好。
我发现自己写了很多类型,这些类型取决于给出的值。
例如,
type Button = {
size: 'small' | 'large';
isReallyBig?: boolean;
appearance: 'solid' | 'outline' | 'minimal';
hasFancyOutline?: boolean;
outlineBackgroundColor: string;
isDisabled?: boolean;
isLoading?: boolean;
}
说我想做:
isReallyBig?
时接受size = 'large'
hasFancyOutline?
和outlineBackgroundColor
仅在appearance = ‘outline’
和isDisabled = false
isLoading
仅在true
时可以是isDisabled = 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 }
]
]
>
这是可以实现的,还是有更好的方法来应对这种情况?
答案 0 :(得分:1)
在执行此操作时,我遇到的问题是,不清楚为什么只有appearance
的值才应从普通情况中删除。 isDisabled
是true | false
的并集,因此,从普通情况中删除所有值将导致在默认情况下从false
中删除isDisabled
。这可能不是所需的行为。
如果我们添加一个属性来说明区别是什么,我们可以构建您想要的类型
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
按钮。