Інтерфейс машинопису - чи можна зробити необхідними властивості “одного чи іншого”?


81

Можливо, дивне запитання, але мені цікаво, чи можливо зробити інтерфейс, де потрібна одна або інша властивість.

Так, наприклад ...

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

interface Attachment {...}

У наведеному вище випадку я хотів би переконатися, що це textабо attachmentіснує.

Сподіваємось, це має сенс.

Спасибі заздалегідь!


Редагувати: Ось як я це роблю зараз. Думав, це трохи багатослівно (набравши боткіт для слабини).

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;
}

Погляньте на моє запропоноване рішення тут .
— ford04

Відповіді:


84

Для цього можна використовувати тип об’єднання:

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);

5
але це не дозволить йому мати повідомлення з текстом та додатком
— Роберто

1
Дякую за відповідь Райане! Ось так я насправді зараз це роблю. Я додав це до свого питання вище. Не був впевнений, чи існує чистіший спосіб це зробити.
— dsifford

6
@Roberto Але це чи не виключає! Це приймають обидва.
— Alessandro_russo

6
Відповідь не працює належним чином , тип Messageприймає {text: 'a', attachment: 'b'} typescriptlang.org/play / ...
— golopot

1
Останнє `| (MessageWithText & MessageWithAttachment) `нічого не робить - він поводиться точно так само без нього.
— mik01aj

44

Якщо ви справді переслідуєте "одне чи інше властивість", а не обидва, ви можете використовувати 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'}

Приклад на дитячому майданчику


3
@golopot, прочитавши ваш коментар, це має бути рішенням, яке ви шукали.
— robstarbuck

3
Це найкращий, він кричить у випадку присвоєння 2-х властивостей, а автозаповнення ввімкнено, на відміну від попередніх відповідей
— Raz Luvaton

Дякую за це. Цей приклад мені допоміг :)
— Yuschick

1
Ця відповідь показує , як автоматизувати цей тип «виключає АБО »: stackoverflow.com/a/61350902/2441655
— Venryx

6

Дякую @ ryan-cavanough, який поставив мене у правильному напрямку.

У мене подібний випадок, але тоді з типами масивів. Трохи боровся із синтаксисом, тому я помістив його тут для подальшого використання:

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
  }
})

9
Просто коротке зауваження, щоб повідомити, що існує безпечний для написання ваш спосіб написання .map()коду. Використовуйте захисний щиток з inоператором. TypeScript зробить правильний висновок щодо типу тощо. Отже, ваш стан буде такимif('requiredPropA' in rule) { /* inside here, TS knows rule is of type <RuleA> */ }
— Ерік Ліпранді

3

Ви можете створити кілька інтерфейсів для необхідних умов і об’єднати їх у такий тип, як тут:

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'
}

2

Є якийсь класний варіант 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 = {

}

Чи є спосіб зробити цей textXOR attachmentза допомогою Omit?
— Chris R,

Будь-який спосіб зробити це сумісним із деструктуризацією параметрів? Я отримую помилки TS, якщо спробую це:
— CletusW

Будь-який спосіб зробити це сумісним із деструктуризацією параметрів? Якщо я спробую, я отримую помилки TSfunction myFunc({ text, attachment }: Message) {}
— CletusW

2

Гаразд, отже, після спроб і помилок та гуглів я виявив, що відповідь не спрацювала належним чином для мого випадку використання. Тож, якщо хтось інший має таку саму проблему, я думав поділитися, як я змусив це працювати. Мій інтерфейс був таким:

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
    }
 }

Просто подумав, що поділюсь цим, бо якби я натрапляв на те трохи дивацтва, то, мабуть, хтось інший теж був.


1

Я натрапив на цю тему, коли шукав відповідь на свій випадок (або propA, propB, або жоден з них). Відповідь Райана Фудзівари майже вийшов, але я втратив деякі перевірки.

Моє рішення:

interface ComponentBase {   
  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;

Це рішення зберігає всі перевірки як слід. Можливо, комусь це стане в нагоді :)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.