web-dev-qa-db-fra.com

TypeScript nécessite un paramètre ou l'autre, mais pas non plus

Dis que j'ai ce type:

export interface Opts {
  paths?: string | Array<string>,
  path?: string | Array<string>
}

Je veux dire à l'utilisateur qu'il doit passer par chemin ou chemin, mais il n'est pas nécessaire de passer par les deux. À l'heure actuelle, le problème est que cela compile:

export const foo = (o: Opts) => {};
foo({});

est-ce que quelqu'un sait autoriser 2 paramètres facultatifs ou plus, mais au moins un paramètre est nécessaire avec TS?

12
Olegzandr Denman

Vous pouvez utiliser

export type Opts = { path: string | Array<string> } | { paths: string | Array<string> }

Pour augmenter la lisibilité, vous pouvez écrire:

type StringOrArray = string | Array<string>;

type PathOpts  = { path : StringOrArray };
type PathsOpts = { paths: StringOrArray };

export type Opts = PathOpts | PathsOpts;
12
Hero Wanders

Si vous avez déjà défini cette interface et souhaitez éviter la duplication des déclarations, vous pouvez éventuellement créer un type conditionnel qui prend un type et renvoie une union avec chaque type de l'union contenant un champ (ainsi qu'un enregistrement de valeurs never pour tous les autres champs, dissoudre les champs supplémentaires à spécifier) 

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

Modifier

Quelques détails sur le type de magie utilisé ici. Nous allons utiliser la propriété distributive des types conditionnels pour itérer sur toutes les clés du type T. La propriété distributive nécessite un paramètre de type supplémentaire pour fonctionner. Nous introduisons TKey à cette fin, mais nous fournissons également une valeur par défaut pour toutes les clés car nous souhaitons prendre toutes les clés de type T

Nous allons donc, en réalité, prendre chaque clé du type d'origine et créer un nouveau type mappé contenant uniquement cette clé. Le résultat sera une union de tous les types mappés contenant une seule clé. Le type mappé supprimera le caractère optionnel de la propriété (le -?, décrit ici ) et la propriété sera du même type que la propriété originale dans T (T[TKey]).

La dernière partie à expliquer est Partial<Record<Exclude<keyof T, TKey>, never>>. En raison du fonctionnement excessif des contrôles de propriété sur les littéraux d'objet, nous pouvons spécifier n'importe quel champ de l'union dans une clé d'objet qui lui est affectée. C’est pour une union telle que { path: string | Array<string> } | { paths: string | Array<string> } que nous pouvons attribuer cet objet littéral { path: "", paths: ""}, ce qui est regrettable. La solution consiste à exiger que si d'autres propriétés de T (autres que TKey afin d'obtenir Exclude<keyof T, TKey>) soient présentes dans l'objet littéral d'un membre d'union donné, elles soient de type never (nous obtenons donc Record<Exclude<keyof T, TKey>, never>>). Mais nous ne voulons pas avoir à spécifier explicitement never pour tous les membres, c'est pourquoi nous avons Partial l'enregistrement précédent.

Cela marche.

Il accepte un type générique T, dans votre cas un string.

Le type générique OneOrMore définit soit 1 de T, soit un tableau de T.

Votre type d'objet d'entrée générique Opts est soit un objet avec une clé path de OneOrMore<T>, soit une clé paths de OneOrMore<T>. Bien que ce ne soit pas vraiment nécessaire, j’ai expliqué clairement que la seule autre option n’est jamais acceptable.

type OneOrMore<T> = T | T[];

export type Opts<T> = { path: OneOrMore<T> } | { paths: OneOrMore<T> } | never;

export const foo = (o: Opts<string>) => {};

foo({});

Il y a une erreur avec {}

2
MikingTheViking

Il n'y a pas de fonctionnalité intégrée pour le faire.

Sauf si vous voulez vérifier dans la fonction les paramètres valides, vous pouvez accepter un dictionnaire comme celui-ci:

foo(paths : {paths: string, path: string} | {paths: Array<string>, path: Array<string>}){};

alors la fonction acceptera:

foo({paths: 'hello', path: 'hello'});

vous pouvez également envisager d'utiliser un tableau à la place:

foo(paths : [string, string] | [Array<string>, Array<string>]){}

qui s'appellera comme ceci: 

foo(['hello', 'hello']); 

mais cette approche est moins compréhensible.

0
Hagai Wild