首页 > 解决方案 > 仅在特定情况下在 TS 接口中设置密钥

问题描述

我正在尝试为“订单”创建一个界面。“书”的订单是一种特殊情况,它应该有一个必填的“标题”字段。那么如何创建 IBookOrder 并将其与 IOtherOrder 结合以制作通用订单界面?

type productType = "book" | "pen" | "notebook"

interface IOtherOrder {
    product: productType, //all productType(s) except book
    amount: number,
    productCode: number,
    description: string
}

interface IBookOrder {
    product: productType, //how do I fix this to "book" type
    amount: number,
    productCode: number,
    description: string,
    title: string,        //required field only for order of "book"
}

//need to combine these here
interface IOrderValues extends IBookOrder, IOtherOrder {} 

const penOrder: IOrderValues = {
    product: "pen",
    amount: 5,
    description: "ink pen",
    productCode: 123,
    title: "This should not be allowed" // <<= incorrect
}

const bookOrder: IOrderValues = {
    product: "book",
    amount: 15,
    description: "Awesome TS book",
    productCode: 321,
    title: "Typescript 101" // <<= this should be required
}

标签: typescript

解决方案


您可以将和的product属性显式标记为您想要的联合的子集:IOtherOrderIBookOrderProductType

interface IOtherOrder {
    product: "pen" | "notebook" // or Exclude<ProductType, "book">
    amount: number,
    productCode: number,
    description: string
}

interface IBookOrder {
    product: "book"
    amount: number,
    productCode: number,
    description: string,
    title: string,
}

请注意,您根本不需要ProductType在这些定义中使用,但是如果您认为以后可能会动态地将更多字符串文字类型添加到联合中,那么Exclude<ProductType, "book">(使用实用程序 type )Exclude<T, U>类的东西也会自动添加它们。


当涉及到您的IOrderValues类型时,您不能将其设为特定(非泛型)interface类型;接口类型必须具有静态已知的成员,并且条件包含title是不可能的。您当然不希望它同时扩展and IBookOrderIOtherOrder因为这样IOrderValues需要 each都是它们两者(现在这是不可能的,因为它们的product属性是互斥的)。

相反,您可能希望它是IBookOrderIOtherOrder接口的联合;如果你想给它一个名字,你可以通过type别名来做到这一点:

type IOrderValues = IBookOrder | IOtherOrder;

(请注意,这会使您的I前缀用词不当;是否需要此类前缀取决于您,并且命名约定可能超出了此问题的范围,因此我不会在这里继续讨论。)


现在您的示例代码的行为符合要求:

const penOrder: IOrderValues = {
    product: "pen",
    amount: 5,
    description: "ink pen",
    productCode: 123,
    title: "This should not be allowed" // error!
//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//  Object literal may only specify known properties, 
//  and 'title' does not exist in type 'IOtherOrder'
}

const bookOrder: IOrderValues = {
    product: "book",
    amount: 15,
    description: "Awesome TS book",
    productCode: 321,
    title: "Typescript 101" 
} // okay

请记住,尽管您看到的错误penOrder依赖于过多的属性检查,只有当您将对象文字直接分配给期望具有较少属性的类型的地方时才会出现这种情况。如果你以不同的方式做事,你最终可能会得到额外的属性:

const obj = {
    product: "pen" as const,
    amount: 5,
    description: "ink pen",
    productCode: 123,
    title: "My wonderful rainbow ink pen",
    numberOfColors: 7
}
const penOrder: IOrderValues = obj; // no error

多余的属性本身并不违反类型系统;TypeScript 具有结构子类型,因此虽然IOtherOrder要求在声明中具有属性,但不禁止具有额外的属性。事实上,这样的额外属性允许支持接口和类扩展;没有它,接口和类层次结构不会形成类型层次结构,并且事情会更难使用。

我个人认为这很好,你不应该担心多余的属性会偷偷溜进去;您不能真正防止所有可能的多余属性,因此您可能应该确保您的代码不依赖于它们的缺失(例如,如果它们存在,它应该简单地忽略它们;它不应该期望Object.keys()只返回已知的键......请参阅为什么 Object.keys 在 TypeScript 中不返回 keyof 类型?)。

如果您想明确排除title出现在 中IOtherOrder,您可以更改IOtherOrder声明:

interface IOtherOrder {
    product: Exclude<ProductType, "book">
    amount: number,
    productCode: number,
    description: string
    title?: never; // <-- add this
}

说这title是一个可选属性,其值将是不可能的never类型。因此 的唯一有效值titleundefined。不过,这可能有点矫枉过正。

Playground 代码链接


推荐阅读