Создать интерфейс в машинописном тексте, в котором разрешено только одно из двух свойств?

Допустим, у меня есть интерфейс

interface ICart {
    property1?: string,
    propert2?: string,
    someOtherProperty: string
}

Как я могу добиться, чтобы разрешено было только одно из property1 и property2, но одно из них должно быть там?


person usr48    schedule 09.11.2019    source источник
comment
Сделайте объединение двух типов: typescriptlang.org/docs/ справочник /   -  person jonrsharpe    schedule 09.11.2019
comment
{property1: string, property2?: never, someOtherProperty: string} | {property1?: never, property2: string, someOtherProperty: string} должно работать   -  person jcalz    schedule 09.11.2019
comment
@jcalz есть ли краткий способ сделать это в случае большого интерфейса   -  person usr48    schedule 09.11.2019
comment
Нравится это?   -  person jcalz    schedule 09.11.2019
comment
@jcalz, спасибо, я думаю, что это может быть что-то, что я могу использовать, но я все еще новичок в машинописном тексте, поэтому может потребоваться дополнительное чтение, чтобы понять это.   -  person usr48    schedule 09.11.2019
comment
Или этого возможно   -  person jcalz    schedule 09.11.2019
comment
@jcalz вау, спасибо, будет ли работать этот фрагмент, если я просто изменил { someOtherProperty: string } на {someOtherProperty1:string, someOtherProperty2: string //and so on}   -  person usr48    schedule 09.11.2019
comment
Я не знаю, потому что не знаю, каков ваш вариант использования. В связанном коде для OneKeyFrom<T, U> потребуются все свойства из U и ровно одно свойство из T. Поэтому, если вам нужно больше свойств, добавьте их в U. Если у вас есть другие свойства для переключения, добавьте их в T. Я напишу это как ответ, когда у меня будет время (если только кто-нибудь не доберется до него первым). Удачи!   -  person jcalz    schedule 09.11.2019


Ответы (2)


Если вы хотите разрешить только одно свойство из списка, вам понадобится объединение типов объектов, каждый из которых разрешает одно свойство и запрещает все остальные. TypeScript не позволяет вам запретить конкретное свойство, но вы можете сделать что-то близкое: сделать его необязательным свойством с типом значения never. На практике это позволит использовать свойство типа undefined, но нет большой разницы между свойствами undefined и отсутствующими свойствами (и разница в любом случае плохо фиксируется в TypeScript).

Итак, для вашего примера выше желаемый тип выглядит так:

type ICartManual = {
    property1: string;
    property2?: undefined;
    someOtherProperty: string;
} | {
    property1?: undefined;
    property2: string;
    someOtherProperty: string;
}

И вы можете убедиться, что он ведет себя так, как вам хочется:

const i1: ICartManual = {
    property1: "prop1",
    someOtherProperty: "other"
}

const i2: ICartManual = {
    property2: "prop2",
    someOtherProperty: "other"
}

const iBoth: ICartManual = { // error!
//    ~~~~~ <-- property1 is incompatible with undefined
    property1: "prop1",
    property2: "prop2",
    someOtherProperty: "other"
}

const iNeither: ICartManual = { // error!
//    ~~~~~~~~ <-- property2 is missing
    someOtherProperty: "other"
}

Если у вас большой интерфейс и вы хотите взять два типа объектов T и U и создать новый, для которого требуется ровно одно свойство из T и все свойства из U, вы можете определить его следующим образом:

type OneKeyFrom<T, M = {}, K extends keyof T = keyof T> = K extends any ?
    (M & Pick<Required<T>, K> & Partial<Record<Exclude<keyof T, K>, never>>) extends infer O ?
    { [P in keyof O]: O[P] } : never : never;

Для этого используется набор mapped и условные типы для создания союза, который вы хотеть. Я мог бы объяснить, как это работает, но для этого потребуется много слов. Я делал подобное раньше; посмотрите здесь для более подробного описания подобного типа.

В любом случае, теперь мы можем определить ICart вот так:

type ICart = OneKeyFrom<{ property1: string, property2: string }, { someOtherProperty: string }>;

и вы можете проверить (например, с помощью IntelliSense), что он совпадает с написанным вручную типом (за исключением порядка, в котором написаны свойства, который не меняет тип):

/* type ICart = {
    property1: string;
    property2?: undefined;
    someOtherProperty: string;
} | {
    property2: string;
    property1?: undefined;
    someOtherProperty: string;
} */

Хорошо, надеюсь, что это поможет; удачи!

Ссылка на код

person jcalz    schedule 09.11.2019

// utility type which blocks two properties of the object coexisting 
type NeverTogether<A extends object, Key1 extends keyof A, Key2 extends keyof A extends Key1 ? never : keyof A> = 
  Omit<A, Key1 | Key2> & (({
    [k in Key1]: A[Key1]
  } & {[k in Key2]?: never}) | ({
    [k in Key1]?: never
  } & {[k in Key2]: A[Key2]}))

interface ICart {
    property1: string,
    property2: string,
    someOtherProperty: string
}

type IC = NeverTogether<ICart, 'property1', 'property2'>;

// error never together
const a: IC = {
  property1: '1',
  property2: '2',
  someOtherProperty: '2'
}

// error one needs to be there
const b: IC = {
  someOtherProperty: '2'
}

// correct
const c: IC = {
  property2: '2',
  someOtherProperty: '2'
}

// correct
const d: IC = {
  property1: '1',
  someOtherProperty: '2'
}

Проблема, связанная с типом NeverTogether, заключается в его составлении, чтобы иметь такое правило для большего количества ключей. Так хорошо работает для двух зависимых полей, но не может работать больше. Но, возможно, это вам поможет. Для меня это была приятная головоломка.

person Maciej Sikora    schedule 10.11.2019