TypeScript требует один параметр или другой, но не
Скажем, у меня такой тип:
export interface Opts {
paths?: string | Array<string>,
path?: string | Array<string>
}
Я хочу сказать пользователю, что они должны проходить либо пути, либо путь, но нет необходимости передавать оба. Сейчас проблема заключается в том, что это компилируется:
export const foo = (o: Opts) => {};
foo({});
кто-нибудь знает, чтобы разрешить 2 или более факультативных, но по крайней мере 1 является необходимым параметром с TS?
Ответы
Ответ 1
Вы можете использовать
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;
Ответ 2
Если вы уже определили этот интерфейс и хотите избежать дублирования деклараций, может возникнуть необходимость создать условный тип, который принимает тип и возвращает объединение с каждым типом в объединении, содержащем одно поле (а также запись 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: string | Array<string> } | { paths: string | Array<string> }
{ 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
предыдущую запись.
Ответ 3
Это работает.
Он принимает общий тип T
, в вашем случае string
.
Общий тип OneOrMore
определяет либо 1 из T
либо массив T
Тип Opts
типа входных объектов 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({});
Произошла ошибка с {}
Ответ 4
Для этого нет встроенной функции.
Если вы не хотите проверять внутри функции для действительных параметров, то вы можете сделать так:
foo(paths : {paths: string, path: string} | {paths: Array<string>, path: Array<string>}){};
поэтому функция примет:
foo({paths: 'hello', path: 'hello'});
вы можете также рассмотреть возможность использования массива:
foo(paths : [string, string] | [Array<string>, Array<string>]){}
который будет называться так:
foo(['hello', 'hello']);
но этот подход менее понятен.