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

но этот подход менее понятен.