Types conditionnels dans TypeScript

66

Je me demandais si je pouvais avoir des types conditionnels dans TypeScript?

Actuellement, j'ai l'interface suivante:

interface ValidationResult {
  isValid: boolean;
  errorText?: string;
}

Mais je veux retirer errorText, et seulement quand isValidest falsecomme nécessaire la propriété.

J'aimerais pouvoir l'écrire comme l'interface suivante:

interface ValidationResult {
  isValid: true;
}

interface ValidationResult {
  isValid: false;
  errorText: string;
}

Mais comme vous le savez, ce n'est pas possible. Alors, quelle est votre idée de cette situation?

Arman
la source
Voulez - vous dire, quand isValidest false?
CertainPerformance
18
isValid est alors redondant. Vous pourriez aussi bien avoir simplement errorText, puis si errorText est nul, il n'y a pas d'erreur.
MTilsted
Oui, @MTilsted, vous avez raison, mais nous devons le conserver en raison de nos anciens codes.
Arman

Réponses:

89

Une façon de modéliser ce type de logique consiste à utiliser un type d'union, quelque chose comme ça

interface Valid {
  isValid: true
}

interface Invalid {
  isValid: false
  errorText: string
}

type ValidationResult = Valid | Invalid

const validate = (n: number): ValidationResult => {
  return n === 4 ? { isValid: true } : { isValid: false, errorText: "num is not 4" }
}

Le compilateur est alors en mesure de restreindre le type en fonction de l'indicateur booléen

const getErrorTextIfPresent = (r: ValidationResult): string | null => {
  return r.isValid ? null : r.errorText
}
Bugs
la source
7
Bonne réponse. Et fascinant que le compilateur puisse réellement dire quer doit être de type Invalidici.
sleske
1
C'est ce qu'on appelle les syndicats discriminés. Des trucs plutôt cool: typescriptlang.org/docs/handbook/…
Umur Kontacı
41

Pour éviter de créer plusieurs interfaces qui ne sont utilisées que pour en créer une troisième, vous pouvez également alterner directement, avec un à la typeplace:

type ValidationResult = {
    isValid: false;
    errorText: string;
} | {
    isValid: true;
};
CertainPerformance
la source
20

L' union démontrée par les bogues est la façon dont je recommande de gérer cela. Néanmoins, Tapuscrit n'ont quelque chose appelé « types conditionnels » , et ils peuvent gérer cela.

type ValidationResult<IsValid extends boolean = boolean> = (IsValid extends true
    ? { isValid: IsValid; }
    : { isValid: IsValid; errorText: string; }
);


declare const validation: ValidationResult;
if (!validation.isValid) {
    validation.errorText;
}

Cette ValidationResult (qui est en fait ValidationResult<boolean>dû au paramètre par défaut) est équivalent à l'union produite dans la réponse des bogues ou dans la réponse de CertainPerformance , et peut être utilisé de la même manière.

L'avantage ici est que vous pouvez également faire circuler une ValidationResult<false>valeur connue , et vous n'aurez alors pas à tester isValidcar elle serait connue falseet errorStringexisterait. Probablement pas nécessaire pour un cas comme celui-ci - et les types conditionnels peuvent être complexes et difficiles à déboguer, donc ils ne devraient probablement pas être utilisés inutilement. Mais vous le pouviez, et cela semblait utile de le mentionner.

KRyan
la source
3
Je suis sûr que ces informations sont parfois très utiles. Mais, pour moi, la syntaxe est vraiment désagréable.
Peilonrayz
3
@Peilonrayz Eh, il a une bonne cohérence avec les autres codages Typescript et extendsest le bon opérateur à utiliser. Et il a très puissant, d' autant plus que vous pouvez également l' utiliser pour creuser dans les types: type SecondOf<T> = T extends Pair<any, infer U> ? U : never;.
KRyan
@KRyan J'y ai pensé. Est-ce que cela signifie simplement SecondOf<number>«se développe» Pair<any, number>? Je suppose que le dicton "ne jugez pas un livre par sa couverture" est pertinent ici.
Peilonrayz
@Peilonrayz Ah, non; plutôt le contraire. SecondOf<Pair<any, number>>évalue à number. SecondOf<number>évalue à never, parce que number extends Pair<any, infer U>c'est faux, comme numberne prolonge aucunPair
KRyan