TypeScript требует один параметр или другой, но не ни - PullRequest
0 голосов
/ 02 сентября 2018

Скажите, у меня есть этот тип:

export interface Opts {
  paths?: string | Array<string>,
  path?: string | Array<string>
}

Я хочу сказать пользователю, что он должен проходить либо пути, либо пути, но нет необходимости проходить оба. Сейчас проблема в том, что это компилируется:

export const foo = (o: Opts) => {};
foo({});

Кто-нибудь знает, чтобы разрешить 2 или более необязательных, но по крайней мере 1 обязательные параметры с TS?

Ответы [ 3 ]

0 голосов
/ 02 сентября 2018

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

export interface Opts {
    paths?: string | Array<string>,
    path?: string | Array<string>
}

type EitherField<T, TKey extends keyof T = keyof T> =
    TKey extends keyof T ? { [P in TKey]-?:T[TKey] } & Partial<Record<Exclude<keyof T, TKey>, never>>: never
export const foo = (o: EitherField<Opts>) => {};
foo({ path : '' });
foo({ paths: '' });
foo({ path : '', paths:'' }); // error
foo({}) // error

Редактировать

Несколько подробностей о магии типов, используемой здесь Мы будем использовать дистрибутивное свойство условных типов , чтобы фактически выполнять итерацию по всем ключам типа T. Для работы дистрибутивного свойства требуется дополнительный параметр типа, и для этой цели мы вводим TKey, но мы также предоставляем значения по умолчанию для всех ключей, поскольку мы хотим получить все ключи типа T.

Итак, мы на самом деле возьмем каждый ключ исходного типа и создадим новый сопоставленный тип, содержащий только этот ключ. Результатом будет объединение всех сопоставленных типов, которые содержат один ключ. Сопоставленный тип удалит необязательность свойства (-?, описанное здесь ), и свойство будет того же типа, что и исходное свойство в T (T[TKey]).

Последняя часть, которая нуждается в объяснении, - Partial<Record<Exclude<keyof T, TKey>, never>>. Из-за того, как работают проверки избыточных свойств для литералов объекта, мы можем указать любое поле объединения в назначенном ему ключе объекта. То есть для объединения, такого как { path: string | Array<string> } | { paths: string | Array<string> }, мы можем присвоить этому объекту литерал { path: "", paths: ""}, что является неудачным. Решение состоит в том, чтобы требовать, чтобы в любом объектном литерале присутствовали какие-либо другие свойства T (отличные от TKey, поэтому мы получили Exclude<keyof T, TKey>), они должны иметь тип never (поэтому мы получаем Record<Exclude<keyof T, TKey>, never>>). Но мы не хотим явно указывать never для всех членов, поэтому мы Partial предыдущую запись.

0 голосов
/ 12 сентября 2018

Это работает.

Он принимает универсальный тип T, в вашем случае string.

Универсальный тип OneOrMore определяет либо 1 из T, либо массив T.

Ваш общий тип входного объекта Opts является либо объектом с ключом path, равным OneOrMore<T>, либо ключом paths, равным OneOrMore<T>. Хотя это и не обязательно, я сделал это явно, так как единственный другой вариант никогда не будет приемлемым.

type OneOrMore<T> = T | T[];

export type Opts<T> = { path: OneOrMore<T> } | { paths: OneOrMore<T> } | never;

export const foo = (o: Opts<string>) => {};

foo({});

Произошла ошибка с {}

0 голосов
/ 02 сентября 2018

Вы можете использовать

export type Opts = { path: string | Array<string> } | { paths: string | Array<string> }

Для повышения читабельности вы можете написать:

type StringOrArray = string | Array<string>;

type PathOpts  = { path : StringOrArray };
type PathsOpts = { paths: StringOrArray };

export type Opts = PathOpts | PathsOpts;
...