英文:
Using TypeScript, how can I change interface based on the value of a property?
问题
我想要在一个对象上强制存在一个属性,只要另一个属性使用特定值。
在以下示例中,当variant
值为ButtonVariant.Subtle
时,我希望backgroundSurfaceValue
是必需的。我正在尝试使用Distributive Conditional Types来解决这个问题,但我可能做错了什么。有没有任何想法我漏掉了什么?
enum ButtonVariant {
Primary = 'Primary',
Secondary = 'Secondary',
Subtle = 'Subtle'
}
type SubtleButtonVariant = ButtonVariant.Subtle
enum SurfaceValue {
Surface0 = 'Surface0',
Surface1 = 'Surface1',
Surface2 = 'Surface2'
}
type ButtonProps = SubtleButtonVariant extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
}
: {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
const subtleButton: ButtonProps = {
variant: ButtonVariant.Subtle,
backgroundSurfaceValue: SurfaceValue.Surface1,
name: 'Subtle Button',
};
const primaryButton: ButtonProps = {
variant: ButtonVariant.Primary,
name: 'Primary Button',
};
英文:
I want to enforce a property being present on an object as long as a specific value is being used for another property.
In the following example, I want the backgroundSurfaceValue
to be required when the variant
value is ButtonVariant.Subtle
. I am trying to solve it using Distributive Conditional Types but I'm not doing something correctly. Any ideas what I'm missing?
Here's a link to the code in the TS playground.
enum ButtonVariant {
Primary = 'Primary',
Secondary = 'Secondary',
Subtle = 'Subtle'
}
type SubtleButtonVariant = ButtonVariant.Subtle
enum SurfaceValue {
Surface0 = 'Surface0',
Surface1 = 'Surface1',
Surface2 = 'Surface2'
}
type ButtonProps = SubtleButtonVariant extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
}
: {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
const subtleButton: ButtonProps = {
variant: ButtonVariant.Subtle,
backgroundSurfaceValue: SurfaceValue.Surface1,
name: 'Subtle Button',
};
const primaryButton: ButtonProps = {
variant: ButtonVariant.Primary,
name: 'Primary Button',
};
答案1
得分: 1
Sure, here's the translated code:
您希望 `ButtonProps` 对象具有一个 `variant` 属性,该属性的[文字类型](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types)可用于*区分*整个对象的多个形状。这表明您确实希望 `ButtonProps` 是一个[区分联合类型](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) ,如下所示:
```typescript
type ButtonProps = {
variant: ButtonVariant.Subtle;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: Exclude<ButtonVariant, ButtonVariant.Subtle>;
backgroundSurfaceValue?: never;
name: string;
};
如果 variant
属性是 ButtonVariant.Subtle
,则 backgroundSurfaceValue
是必需的,并且必须是 SurfaceValue
类型。
否则,如果 variant
属性是 ButtonVariant.Primary
或 ButtonVariant.Secondary
(在上面表示为 Exclude<ButtonVariant, ButtonVariant.Subtle>
通过排除实用程序类型),则 backgroundSurfaceValue
实际上被禁止,它是可选属性 且类型为不可能的 never
类型,这意味着您只能从中读取 undefined
。
让我们测试一下:
const subtleButton: ButtonProps = {
variant: ButtonVariant.Subtle,
backgroundSurfaceValue: SurfaceValue.Surface1,
name: 'Subtle Button',
};
const primaryButton: ButtonProps = {
variant: ButtonVariant.Primary,
name: 'Primary Button',
};
const badButton: ButtonProps = { // 错误!
// ~~~~~~~~~ <--- 缺少属性 'backgroundSurfaceValue'
variant: ButtonVariant.Subtle,
name: 'Bad Button'
}
const alsoBadButton: ButtonProps = { // 错误!
// ~~~~~~~~~~~~~ <--- 类型 'SurfaceValue' 无法分配给类型 'undefined'
variant: ButtonVariant.Secondary,
backgroundSurfaceValue: SurfaceValue.Surface2,
name: 'Also Bad Button'
}
看起来不错。
请注意,您的版本存在一些问题,这些问题会阻止其按预期工作:
它实际上不是一个分布式条件类型,因为所检查的类型 SubtleButtonVariant
不是通用类型参数;它是一个特定类型,相当于 ButtonVariant.Subtle
。这只是一个常规条件类型,由于SubtleButtonVariant extends ButtonVariant.Subtle
总是为真,因此它将仅计算为真分支。
要使其分布,您需要编写类似于以下内容:
type ButtonProps<T extends ButtonVariant> = T extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
}
: {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
但这不会是您可以在不指定输入类型参数的情况下分配给变量的类型。分布式条件类型的整个目的是在其输入的联合上分布某些类型操作。因此,您需要给它一个联合。
您可以通过在另一个类型中指定它来修复那个问题:
type BProps = ButtonProps<ButtonVariant>;
或者通过将 T
设置为 ButtonVariant
的默认值来修复它:
type ButtonProps<T extends ButtonVariant = ButtonVariant> =
T extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
}
: {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
无论哪种方式,都会生成一个新的联合类型:
/* type ButtonProps = {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
}*/
但当然,这仍然不起作用... 联合的两个成员上的 variant
属性是相同的,因此没有什么可以用于区分的。而且,联合的一个成员上的 backgroundSurfaceValue
属性是 never
类型的必需属性,这是不可能的。如果我们修复这两个问题,我们会得到:
type ButtonProps = {
variant: ButtonVariant.Subtle;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: Exclude<ButtonVariant, ButtonVariant.Subtle>;
backgroundSurfaceValue?: never;
name: string;
};
也就是说,您再次获得了一个可区分的联合类型。也许您想要使用通用条件类型来定义 ButtonProps
,但从示例中并不明显。对于所提的问题,您应该使用区分联合类型而不是条件类型,无论是分布式还是其他类型的条件类型。
<details>
<summary>英文:</summary>
You want a `ButtonProps` object to have a `variant` property, whose [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) can be used to *discriminate* which of several shapes the whole object has. That indicates you really want `ButtonProps` to be a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) type, as follows:
type ButtonProps = {
variant: ButtonVariant.Subtle;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: Exclude<ButtonVariant, ButtonVariant.Subtle>;
backgroundSurfaceValue?: never;
name: string;
};
If the `variant` prop is `ButtonVariant.Subtle`, then `backgroundSurfaceValue` is required and must be of type `SurfaceValue`.
Otherwise, if the `variant` prop is either `ButtonVariant.Primary` or `ButtonVariant.Secondary` (which is represented above as `Exclude<ButtonVariant, ButtonVariant.Subtle>` via [the `Exclude` utility type](https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers)), then `backgroundSurfaceValue` is essentially prohibited, being an [optional property](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties) of [the impossible `never` type](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type), meaning the only value you could possibly read from it is `undefined`.
Let's test that out:
const subtleButton: ButtonProps = {
variant: ButtonVariant.Subtle,
backgroundSurfaceValue: SurfaceValue.Surface1,
name: 'Subtle Button',
};
const primaryButton: ButtonProps = {
variant: ButtonVariant.Primary,
name: 'Primary Button',
};
const badButton: ButtonProps = { // error!
// ~~~~~~~~~ <-- Property 'backgroundSurfaceValue' is missing
variant: ButtonVariant.Subtle,
name: 'Bad Button'
}
const alsoBadButton: ButtonProps = { // error!
// ~~~~~~~~~~~~~ <-- Type 'SurfaceValue' is not assignable to type 'undefined'
variant: ButtonVariant.Secondary,
backgroundSurfaceValue: SurfaceValue.Surface2
name: 'Also Bad Button'
}
Looks good.
---
Note that your version had several problems which prevented it from working as desired:
It wasn't actually a [distributive conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types) because the checked type `SubtleButtonVariant` is not a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) type parameter; it's a specific type, equivalent to `ButtonVariant.Subtle`. That's just a regular conditional type, which will evaluate to the true branch alone (since `SubtleButtonVariant extends ButtonVariant.Subtle` is always true).
For it to be distributive, you'd have needed to write something like
type ButtonProps<T extends ButtonVariant> = T extends ButtonVariant.Subtle
? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} : {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
but that wouldn't be a type you could just assign to a variable without specifying the input type parameter. The whole point of distributive conditional types is to *distribute* some type operation across [unions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) in their inputs. So you need to give it a union.
You could have fixed *that* by either specifying it in another type
type BProps = ButtonProps<ButtonVariant>;
or by having `T` [default](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) to `ButtonVariant`:
type ButtonProps<T extends ButtonVariant = ButtonVariant> =
T extends ButtonVariant.Subtle ? {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} : {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
};
Either way would produce a new union type:
/* type ButtonProps = {
variant: ButtonVariant;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: ButtonVariant;
backgroundSurfaceValue: never;
name: string;
}*/
But of course, that still doesn't work... the `variant` property is the same on both halves of the union, so there's nothing to discriminate. Also, the `backgroundSurfaceValue` property on one of the union members is a *required* property of type `never`, which is impossible. If we fix both of those we get:
type ButtonProps = {
variant: ButtonVariant.Subtle;
backgroundSurfaceValue: SurfaceValue;
name: string;
} | {
variant: Exclude<ButtonVariant, ButtonVariant.Subtle>;
backgroundSurfaceValue?: never;
name: string;
};
That is, you get a discriminated union again. There might be some reason why you'd want to use a generic conditional type for `ButtonProps`, but it's not obvious from the example. For the question as asked, you should just use a discriminated union instead of a conditional type, distributive or otherwise.
[Playground link to code](https://www.typescriptlang.org/play?#code/HYQwtgpgzgDiDGEAEApeIA2AvJBvAUEkhMAK5hIBCpALjQPbABqIATgJYjA16FFIAFDmDYBPJAF4kAciHsRrUdIA0fIgGUI8RgBMxkmZu3A9ilWqTrSAIxoZkU6Vdv3pfAL74vREuUulWADMECBYMUmQCfn8gkIAGAycA4MQ482irWMQARkTMlIhs9P58kIAmPOTytyJPPhpRGGRqOkYhehgoAyj+ADc2Tm4ALipaBmYBrhoAOmc7CABuC2sEAGsAc1Z6UhNSxDCIkb3QzAil6NBIEagaDmB186R3JAAfXmj+jimRgFEAD3g4R0EAAPC1xiwvtxlKNWhMoTM5vYAHyPIgreAbLY7HTHA4QAD8I2AEF6EFYaKQlwg11u7Huj3cjwA9MzYRDJtxpnIFOI3uDGJDBoitLoxN4kMYbkgoDZ5gLgCMFe1Ot0LJ9hUqxoLOYi5fZVNEMVjtrsqvtTjSYgV8bNzYVDfxqSMki5mtrgMUmRKpTwYMIxAqtXCVV0pD0iBrvuydQjuQHFI6iM6ZDz9AqvUs+L6kCsdEGY8BQ90kKziKwtqwAIRICxlpAAPybzebSBBAFp24Itk1WA0ZMbNqbcfb8dIkOwumBJ1B6et1brgxy40iIEmqeArdJKCAdIWak8fYxpZgoPQd-mPUu2j2w3hS2zyZWq3W2Y2Wx-Wx2uwAVRrIJIshOcIIHHScqXoHgQCgWd1lAax7CQBgkP-GQcQgQJ6QgHQD0jRdCyFKZZlFEwxHXQdsTNID8SOUdLTtICyj4FNpAAQQwM8qF3fcPAlalYBCbt6AQiAwAAeRJd5+AaJp-DdBVCO4AwFN1O03QlIgZPdENbwMVcVIRYg-hoEgdC6AzhTU+YLCIAkpOiPCEWveFhUpI01iHHE8UtWjqMtNynU3Wk7geGzDwcpARgjByo2GAjdQC9EPMokc-JA4lSXJRKNyuGU6QZMLvVqPjNwExAhJEsAfwAd3oXgLC0wtQxBH8jJMkxzI9RSaGRAxWogYzTM6uFuqs+wwrs6KPnwiypmyijh289LrRCfFspTG4QoC54orCxzNXihF5uSxa6OWkkyQpPacqtTa522ylGsoYspGVW8wS63VUTrAAqFDZOe3TwzC2KaGc7qAoWryzsOFaLRAgKNvy0KHOeN4pqQUHwYSsKoaom0fKpTKroipGtps9wfuZLxeOY0q4HK9pKp-AALVgIEiDT-u08Zmv6waOsO4VlM+hFeokML+fasyhaI1ckEm66sdl7hjsxTz8dWwmlrOa6yfuwrIvsmKZtF1zrrx1KCfO4n1qCvLyYi70GtQwGOjvN73cpZk-qel7jf26NZtV3GTuhtLYZ1xYwv1grUdeAPMdNkacYiy2o4yy7Eftu64+iSnqYlOp3CAA)
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论