Как создать Partial-like, для которого требуется установить одно свойство

У нас есть структура, которая выглядит следующим образом:

export type LinkRestSource = {
    model: string;
    rel?: string;
    title?: string;
} | {
    model?: string;
    rel: string;
    title?: string;
} | {
    model?: string;
    rel?: string;
    title: string;
};

Это почти то же самое, что сказать

type LinkRestSource = Partial<{model: string, rel: string, title: string}>

За исключением того, что это позволит передать пустой объект, тогда как для начального типа требуется одно из свойств, которое должно быть передано в

Как создать общий тип, например Partial, но который ведет себя как моя структура выше?

Ответ 1

Я думаю, у меня есть решение для вас. Вы ищете что-то, что принимает тип T и производит связанный тип, который содержит хотя бы одно свойство из T. То есть он похож на Partial<T>, но исключает пустой объект.

Если это так, вот оно:

type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]

Рассекать: прежде всего, AtLeastOne<T> пересекается с чем-то Partial<T>. U[keyof U] означает, что это объединение всех значений свойств U. И я определил (значение по умолчанию) U как сопоставленный тип, где каждое свойство T сопоставлено с Pick<T, K>, типом с одним свойством для ключа K, (Например, Pick<{foo: string, bar: number},'foo'> эквивалентно {foo: string}... он "выбирает" свойство 'foo' из исходного типа.) Это означает, что U[keyof U] в этом случае является объединением всех возможных типов с одним свойством из T.

Хм, это может сбить с толку. Давайте посмотрим шаг за шагом, как он работает со следующим конкретным типом:

type FullLinkRestSource = {
  model: string;
  rel: string;
  title: string;
}

type LinkRestSource = AtLeastOne<FullLinkRestSource>

Это расширяется до

type LinkRestSource = AtLeastOne<FullLinkRestSource, {
  [K in keyof FullLinkRestSource]: Pick<FullLinkRestSource, K>
}>

или

type LinkRestSource = AtLeastOne<FullLinkRestSource, {
  model: Pick<FullLinkRestSource, 'model'>,
  rel: Pick<FullLinkRestSource, 'rel'>,
  title: Pick<FullLinkRestSource, 'title'>
}>

или

type LinkRestSource = AtLeastOne<FullLinkRestSource, {
  model: {model: string},
  rel: {rel: string},
  title: {title: string}>
}>

или

type LinkRestSource = Partial<FullLinkRestSource> & {
  model: {model: string},
  rel: {rel: string},
  title: {title: string}>
}[keyof {
  model: {model: string},
  rel: {rel: string},
  title: {title: string}>
}]

или

type LinkRestSource = Partial<FullLinkRestSource> & {
  model: {model: string},
  rel: {rel: string},
  title: {title: string}>
}['model' | 'rel' | 'title']

или

type LinkRestSource = Partial<FullLinkRestSource> &
  ({model: string} | {rel: string} | {title: string})

или

type LinkRestSource = {model?: string, rel?: string, title?: string} & 
  ({model: string} | {rel: string} | {title: string})

или

type LinkRestSource = { model: string, rel?: string, title?: string } 
  | {model?: string, rel: string, title?: string} 
  | {model?: string, rel?: string, title: string}

что, я думаю, то, что вы хотите.

Вы можете проверить это:

const okay0: LinkRestSource = { model: 'a', rel: 'b', title: 'c' }
const okay1: LinkRestSource = { model: 'a', rel: 'b' }
const okay2: LinkRestSource = { model: 'a' }
const okay3: LinkRestSource = { rel: 'b' }
const okay4: LinkRestSource = { title: 'c' }

const error0: LinkRestSource = {} // missing property
const error1: LinkRestSource = { model: 'a', titel: 'c' } // excess property on string literal

Итак, это работает для вас? Удачи!

Ответ 2

Это невозможно с Partial<T>. Под капотом это выглядит так:

type Partial<T> = { [P in keyof T]?: T[P]; };

Все свойства сделаны необязательными.

Я сомневаюсь, что возможно (или легко) обеспечить соблюдение вашего правила через систему типов.

Можно попытаться создать тип, который использует keyof аналогичным образом, но имеет условие в конструкторе по умолчанию.

Если вы можете придумать способ объявления типа типа Partial, который строит матрицу типов типа yours, испускающих ? для разных ключей в каждом и concat все из них, используя |, как в вашем первом примере, возможно, вы сможете применить свою систему правил типа vie.

Это сообщение в блоге по ключевому слову может дать вам некоторые идеи.

Ответ 3

Есть другое решение, если вы знаете, какие свойства вы хотите.

AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>

Это также позволит вам заблокировать несколько ключей типа, например, AtLeast<T, 'model' | 'rel'>.

Ответ 4

Может быть, что-то вроде этого:

type X<A, B, C> = (A & Partial<B> & Partial<C>) | (Partial<A> & B & Partial<C>) | (Partial<A> & Partial<B> & C);
type LinkRestSource = X<{ model: string }, { rel: string }, { title: string }>
var d: LinkRestSource = {rel: 'sdf'};  

Но это немного грязно:)

или

type Y<A, B, C> = Partial<A & B & C> & (A | B | C);