【问题标题】:Typescript Interface - Possible to make "one or the other" properties required?打字稿界面 - 可能需要“一个或另一个”属性?
【发布时间】:2016-10-07 21:13:50
【问题描述】:

可能是一个奇怪的问题,但我很好奇是否可以创建一个需要一个属性或另一个属性的接口。

所以,例如...

interface Message {
    text: string;
    attachment: Attachment;
    timestamp?: number;
    // ...etc
}

interface Attachment {...}

在上述情况下,我想确保 textattachment 存在。


这就是我现在的做法。认为它有点冗长(为 slack 键入 botkit)。

interface Message {
    type?: string;
    channel?: string;
    user?: string;
    text?: string;
    attachments?: Slack.Attachment[];
    ts?: string;
    team?: string;
    event?: string;
    match?: [string, {index: number}, {input: string}];
}

interface AttachmentMessageNoContext extends Message {
    channel: string;
    attachments: Slack.Attachment[];
}

interface TextMessageNoContext extends Message {
    channel: string;
    text: string;
}

【问题讨论】:

  • 看看我建议的解决方案here

标签: typescript


【解决方案1】:

如果您真正追求“一个属性或另一个”而不是两者,您可以在扩展类型中使用never

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
  attachment?: never;
}
interface MessageWithAttachment extends MessageBasics {
  text?: never;
  attachment: string;
}
type Message = MessageWithText | MessageWithAttachment;

// ? OK 
let foo: Message = {attachment: 'a'}

// ? OK
let bar: Message = {text: 'b'}

// ❌ ERROR: Type '{ attachment: string; text: string; }' is not assignable to type 'Message'.
let baz: Message = {attachment: 'a', text: 'b'}

Example in Playground

【讨论】:

  • @golopot,在阅读了您的评论后,这应该是您所追求的解决方案。
  • 这是最好的一个,它在分配 2 个属性的情况下会尖叫,并且与之前的答案不同,它启用了自动完成
  • 这个答案展示了如何自动化这个“XOR”类型:stackoverflow.com/a/61350902/2441655
【解决方案2】:

您可以使用联合类型来执行此操作:

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
}
interface MessageWithAttachment extends MessageBasics {
  attachment: Attachment;
}
type Message = MessageWithText | MessageWithAttachment;

如果你想同时允许文本和附件,你会写

type Message = MessageWithText | MessageWithAttachment | (MessageWithText & MessageWithAttachment);

【讨论】:

  • 但这不会让他收到带有文本和附件的消息
  • 感谢瑞恩的回复!这就是我现在实际上正在做的事情。我在上面的问题中添加了这一点。不确定是否有更清洁的方法。
  • @Roberto 但是这还是不排除!这两者都接受。
  • 答案不按预期工作,输入Message接受{text: 'a', attachment: 'b'}typescriptlang.org/play/…
  • 最后一个` | (MessageWithText & MessageWithAttachment)` 什么都不做 - 没有它,它的行为完全相同。
【解决方案3】:

您可以更深入地使用@robstarbuck 解决方案创建以下类型:

type Only<T, U> = {
  [P in keyof T]: T[P];
} & {
  [P in keyof U]?: never;
};

type Either<T, U> = Only<T, U> | Only<U, T>;

然后Message 类型看起来像这样

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
}
interface MessageWithAttachment extends MessageBasics {
  attachment: string;
}
type Message = Either<MessageWithText, MessageWithAttachment>;

使用此解决方案,您可以轻松地在 MessageWithTextMessageWithAttachment 类型中添加更多字段,而不会在另一个中排除它。

【讨论】:

  • 很棒的通用解决方案。如果您的接口的通用属性在键中相同但具有某些不同的值,您可以使用Only 的修改版本:type Only&lt;T, U&gt; = { [P in keyof T]: T[P] } &amp; Omit&lt;{ [P in keyof U]?: never }, keyof T&gt;
  • 这是一个拯救生命的解决方案。道具给你。我很惊讶 Typescript 无法更轻松地做到这一点。
【解决方案4】:

感谢@ryan-cavanaugh 让我朝着正确的方向前进。

我也有类似的情况,但使用的是数组类型。语法有点吃力,所以我把它放在这里供以后参考:

interface BaseRule {
  optionalProp?: number
}

interface RuleA extends BaseRule {
  requiredPropA: string
}

interface RuleB extends BaseRule {
  requiredPropB: string
}

type SpecialRules = Array<RuleA | RuleB>

// or

type SpecialRules = (RuleA | RuleB)[]

// or (in the strict linted project I'm in):

type SpecialRule = RuleA | RuleB
type SpecialRules = SpecialRule[]

更新:

请注意,稍后,当您在代码中使用声明的变量时,您可能仍会收到警告。然后您可以使用(variable as type) 语法。 示例:

const myRules: SpecialRules = [
  {
    optionalProp: 123,
    requiredPropA: 'This object is of type RuleA'
  },
  {
    requiredPropB: 'This object is of type RuleB'
  }
]

myRules.map((rule) => {
  if ((rule as RuleA).requiredPropA) {
    // do stuff
  } else {
    // do other stuff
  }
})

【讨论】:

  • 只是一个简短的说明,让您知道有一种类型安全、智能感知友好的方式来编写您的 .map() 代码。将 type-guard 与 in 运算符一起使用。 TypeScript 会进行正确的类型推断等。所以你的条件是if('requiredPropA' in rule) { /* inside here, TS knows rule is of type &lt;RuleA&gt; */ }
【解决方案5】:

您可以为所需条件创建几个接口,并将它们加入到如下类型中:

interface SolidPart {
    name: string;
    surname: string;
    action: 'add' | 'edit' | 'delete';
    id?: number;
}
interface WithId {
    action: 'edit' | 'delete';
    id: number;
}
interface WithoutId {
    action: 'add';
    id?: number;
}

export type Entity = SolidPart & (WithId | WithoutId);

const item: Entity = { // valid
    name: 'John',
    surname: 'Doe',
    action: 'add'
}
const item: Entity = { // not valid, id required for action === 'edit'
    name: 'John',
    surname: 'Doe',
    action: 'edit'
}

【讨论】:

    【解决方案6】:

    在为我的案例寻找答案(propA、propB 或都没有)时,我偶然发现了这个线程。 Ryan Fujiwara 的回答几乎成功了,但我因此丢失了一些支票。

    我的解决方案:

    interface Base {   
      baseProp: string; 
    }
    
    interface ComponentWithPropA extends Base {
      propA: string;
      propB?: never;
    }
    
    interface ComponentWithPropB extends Base {
      propB: string;
      propA?: never;
    } 
    
    interface ComponentWithoutProps extends Base {
      propA?: never;
      propB?: never;
    }
    
    type ComponentProps = ComponentWithPropA | ComponentWithPropB | ComponentWithoutProps;
    

    此解决方案将所有检查保持原样。也许有人会觉得这很有用:)

    【讨论】:

    • 一个很好的练习可能是创建一个映射类型来复制此行为。
    【解决方案7】:

    您还可以对通用属性使用抽象类而不是接口,以防止有人意外实现该接口。

    abstract class BaseMessage {
      timestamp?: number;
      /* more general properties here */
      constructor(timestamp?: number) {
        this.timestamp = timestamp;
        /* etc. for other general properties */
      }
    }
    interface IMessageWithText extends BaseMessage {
      text: string;
      attachment?: never;
    }
    interface IMessageWithAttachment extends BaseMessage {
      text?: never;
      attachment: string;
    }
    type Message = IMessageWithText | IMessageWithAttachment;
    

    【讨论】:

      【解决方案8】:

      您可以使用一些很酷的 Typescript 选项https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk

      您的问题是:创建一个存在“文本”或附件的界面。你可以这样做:

      interface AllMessageProperties {
        text: string,
        attachement: string,
      }
      
      type Message = Omit<AllMessageProperties, 'text'> | Omit<AllMessageProperties, 'attachement'>;
      
      const messageWithText : Message = {
        text: 'some text'
      }
      
      const messageWithAttachement : Message = {
        attachement: 'path-to/attachment'
      }
      
      const messageWithTextAndAttachement : Message = {
        text: 'some text',
        attachement: 'path-to/attachment'
      }
      
      // results in Typescript error
      const messageWithOutTextOrAttachement : Message = {
      
      }
      

      【讨论】:

      • 有没有办法使用Omit 使text XOR attachment
      • 有什么方法可以使它与参数解构兼容?如果我尝试这个,我会得到 TS 错误:
      • 有什么方法可以使它与参数解构兼容?如果我尝试function myFunc({ text, attachment }: Message) {},我会收到 TS 错误
      【解决方案9】:

      好的,经过反复试验和谷歌搜索后,我发现答案并没有按我的用例预期的那样工作。因此,如果其他人遇到同样的问题,我想我会分享我是如何工作的。我的界面是这样的:

      export interface MainProps {
        prop1?: string;
        prop2?: string;
        prop3: string;
      }
      

      我正在寻找的是一个类型定义,它表明我们既不能定义 prop1 也不能定义 prop2。我们可以定义 prop1,但不能定义 prop2。最后定义了prop2,但没有定义prop1。这是我发现的解决方案。

      interface MainBase {
        prop3: string;
      }
      
      interface MainWithProp1 {
        prop1: string;
      }
      
      interface MainWithProp2 {
        prop2: string;
      }
      
      export type MainProps = MainBase | (MainBase & MainWithProp1) | (MainBase & MainWithProp2);
      

      这很完美,除了一个警告是,当我尝试在另一个文件中引用 prop1 或 prop2 时,我不断收到属性不存在 TS 错误。以下是我解决这个问题的方法:

      import {MainProps} from 'location/MainProps';
      
      const namedFunction = (props: MainProps) => {
          if('prop1' in props){
            doSomethingWith(props.prop1);
          } else if ('prop2' in props){
            doSomethingWith(props.prop2);
          } else {
            // neither prop1 nor prop2 are defined
          }
       }
      

      我只是想分享一下,因为如果我遇到了一点点怪异,那么其他人可能也是。

      【讨论】:

      【解决方案10】:

      到目前为止还没有人提到它,但我认为偶然发现此页面的人也可以考虑使用discriminated unions。如果我正确理解了 OP 代码的含义,那么它可能会变成这样。

      interface Attachment {}
      
      interface MessageBase {
          type?: string;
          user?: string;
          ts?: string;
          team?: string;
          event?: string;
          match?: [string, {index: number}, {input: string}];
      }
      
      interface AttachmentMessageNoContext extends MessageBase {
          kind: 'withAttachments',
          channel: string;
          attachments: Attachment[];
      }
      
      interface TextMessageNoContext extends MessageBase {
          kind: 'justText', 
          channel: string;
          text: string;
      }
      
      type Message = TextMessageNoContext | AttachmentMessageNoContext
      
      const textMessage: Message = {
        kind: 'justText',
        channel: 'foo',
        text: "whats up???" 
      }
      
      const messageWithAttachment: Message = {
        kind: 'withAttachments',
        channel: 'foo',
        attachments: []
      }
      

      现在Message 接口需要attachmentstext,具体取决于kind 属性。

      【讨论】:

        【解决方案11】:

        简单的“需要二选一”示例:

        type Props =
          | { factor: Factor; ratings?: never }
          | { ratings: Rating[]; factor?: never }
        

        【讨论】:

          猜你喜欢
          • 2017-03-23
          • 2021-09-26
          • 2020-07-07
          • 1970-01-01
          • 2017-09-11
          • 2019-12-22
          • 2021-12-24
          • 2021-05-31
          • 2020-11-03
          相关资源
          最近更新 更多