Если у вас уже определен этот интерфейс и вы хотите избежать дублирования объявлений, можно создать условный тип, который принимает тип и возвращает объединение с каждым типом в объединении, содержащем одно поле (а также запись 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
предыдущую запись.