Tipos condicionales en TypeScript

66

Me preguntaba si puedo tener tipos condicionales en TypeScript?

Actualmente tengo la siguiente interfaz:

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

Pero quiero eliminar errorText, y solo tenerlo cuando isValides falsecomo una propiedad requerida .

Desearía poder escribirlo como la siguiente interfaz:

interface ValidationResult {
  isValid: true;
}

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

Pero como sabes, no es posible. Entonces, ¿cuál es su idea sobre esta situación?

Arman
fuente
¿Quieres decir cuándo isValides false?
CertainPerformance
18
isValid es redundante entonces. También podría tener el errorText, y luego si errorText es nulo, no hay error.
MTilsted
Sí, @MTilsted, tiene razón, pero tenemos que mantenerlo debido a nuestros códigos heredados.
Arman

Respuestas:

89

Una forma de modelar este tipo de lógica es usar un tipo de unión, algo como esto

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" }
}

El compilador puede reducir el tipo basado en la bandera booleana

const getErrorTextIfPresent = (r: ValidationResult): string | null => {
  return r.isValid ? null : r.errorText
}
loco
fuente
77
Buena respuesta. Y fascinante que el compilador realmente pueda decir que rdebe ser de tipo Invalidaquí.
sleske
1
Esto se llama sindicatos discriminados. Cosas geniales: typescriptlang.org/docs/handbook/…
Umur Kontacı
41

Para evitar crear múltiples interfaces que solo se utilizan para crear un tercero, también puede alternar directamente, con un typelugar:

type ValidationResult = {
    isValid: false;
    errorText: string;
} | {
    isValid: true;
};
Cierto rendimiento
fuente
20

La unión demostrada por los errores es cómo recomiendo manejar esto. No obstante, Letra de imprenta no tiene algo conocido como “ tipos condicionales ”, y que puede manejar esto.

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


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

Esto ValidationResult(que en realidad se ValidationResult<boolean>debe al parámetro predeterminado) es equivalente a la unión producida en la respuesta de errores o en la respuesta de CertainPerformance , y se puede usar de la misma manera.

La ventaja aquí es que también podría pasar un ValidationResult<false>valor conocido , y luego no tendría que probar isValidcomo se sabe falsey errorStringse sabe que existe. Probablemente no sea necesario para un caso como este, y los tipos condicionales pueden ser complejos y difíciles de depurar, por lo que probablemente no deberían usarse innecesariamente. Pero podrías, y parecía que valía la pena mencionarlo.

KRyan
fuente
3
Estoy seguro de que estos son realmente útiles a veces. Pero, para mí, la sintaxis parece realmente desagradable.
Peilonrayz
3
@Peilonrayz Eh, tiene una buena coherencia con otras codificaciones de Typecript y extendses el operador adecuado para usar. Y tiene muy potente, sobre todo porque también se puede utilizar para profundizar en los tipos: type SecondOf<T> = T extends Pair<any, infer U> ? U : never;.
KRyan
@KRyan He estado pensando en eso. ¿Básicamente, eso solo significa SecondOf<number>'se expande' Pair<any, number>? Supongo que el dicho "no juzgues un libro por su portada" es pertinente aquí.
Peilonrayz
@Peilonrayz Ah, no; más bien lo contrario. SecondOf<Pair<any, number>>evalúa a number. SecondOf<number>evalúa a never, porque number extends Pair<any, infer U>es falso, ya numberque no extiende ningunoPair
KRyan