web-dev-qa-db-fra.com

Comment créer un type partiel qui nécessite la définition d'une seule propriété

Nous avons une structure qui ressemble à ceci:

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

Ce qui revient presque à dire

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

Sauf que cela permet de passer un objet vide alors que le type initial nécessite de passer une des propriétés

Comment puis-je créer un générique comme Partial, mais qui se comporte comme ma structure ci-dessus?

22
Juan Mendes

Je pense que j'ai une solution pour vous. Vous recherchez quelque chose qui prend un type T et produit un type connexe qui contient au moins une propriété de T. Autrement dit, c'est comme Partial<T> mais exclut l'objet vide.

Si oui, le voici:

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

Pour le disséquer: tout d'abord, AtLeastOne<T> est Partial<T> intersecté avec quelque chose . U[keyof U] signifie que c'est l'union de toutes les valeurs de propriété de U. Et j'ai défini (la valeur par défaut de) U pour être un type mappé où chaque propriété de T est mappée sur Pick<T, K>, un type de propriété unique pour la clé K. (Par exemple, Pick<{foo: string, bar: number},'foo'> est équivalent à {foo: string}... il "sélectionne" le 'foo' propriété du type d'origine.) Signifiant que U[keyof U] dans ce cas est l'union de tous les types de propriété unique possibles de T.

Hmm, ça pourrait être déroutant. Voyons étape par étape comment il fonctionne sur le type de béton suivant:

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

type LinkRestSource = AtLeastOne<FullLinkRestSource>

Cela s'étend à

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

ou

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

ou

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

ou

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

ou

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

ou

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

ou

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

ou

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

c'est, je pense, ce que vous voulez.

Vous pouvez le tester:

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

Alors, ça marche pour vous? Bonne chance!

22
jcalz

Il existe une autre solution si vous savez quelles propriétés vous souhaitez.

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

Cela vous permettrait également de verrouiller plusieurs clés d'un type, par exemple AtLeast<T, 'model' | 'rel'>.

1
aegatlin

Peut-être quelque chose comme ça:

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'};  

Mais c'est un peu désordonné :)

ou

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

Une version plus simple de la solution par jcalz:

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

de sorte que toute la mise en œuvre devient

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

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

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

et voici le lien TS aire de jeux pour l'essayer

0
gafi

Ce ne serait pas possible avec Partial<T>. Sous le capot, cela ressemble à ceci:

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

Toutes les propriétés sont rendues facultatives.

Je doute qu'il soit possible (ou facile) d'appliquer votre règle via le système de type.

Pourrait essayer de créer un type qui emploie keyof d'une manière similaire, mais qui a une condition dans le constructeur par défaut.

Si vous pouvez penser à un moyen de déclarer un type comme Partial qui construit une matrice de types comme le vôtre émettant ? pour une clé différente dans chacune et concattez-les toutes en utilisant | comme dans votre premier exemple, vous pourrez peut-être appliquer votre système de type de règle de vie.

Ce billet de blog sur keyof pourrait vous donner quelques idées.

0
Justinas Marozas