Tipos condicionais no TypeScript

65

Eu queria saber se posso ter tipos condicionais no TypeScript?

Atualmente, tenho a seguinte interface:

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

Mas quero removê-lo errorTexte tê-lo somente quando isValidé falseuma propriedade necessária .

Eu gostaria de poder escrevê-lo como a seguinte interface:

interface ValidationResult {
  isValid: true;
}

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

Mas como você sabe, não é possível. Então, qual é a sua ideia sobre essa situação?

Arman
fonte
Você quer dizer quando isValidé false?
CertainPerformance 04/12/19
18
isValid é redundante então. Você também pode ter apenas o errorText e, se errorText for nulo, não haverá erro.
MTilsted 04/12/19
Sim, @MTilsted, você está certo, mas precisamos mantê-lo por causa dos nossos códigos herdados.
Arman

Respostas:

89

Uma maneira de modelar esse tipo de lógica é usar um tipo de união, algo como isto

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

O compilador pode restringir o tipo com base no sinalizador booleano

const getErrorTextIfPresent = (r: ValidationResult): string | null => {
  return r.isValid ? null : r.errorText
}
insetos
fonte
7
Boa resposta. E fascinante que o compilador possa realmente dizer que rdeve ser do tipo Invalidaqui.
sleske
11
Isso é chamado de união discriminada. Coisas bem legais: typescriptlang.org/docs/handbook/…
Umur Kontacı
41

Para evitar a criação de várias interfaces que apenas são usadas para criar uma terceira, você também pode alternar diretamente, com uma alternativa type:

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

A união demonstrada pelos bugs é como eu recomendo lidar com isso. No entanto, Typescript não tem algo conhecido como “ tipos condicionais ,” e eles podem lidar com isso.

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


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

Isso ValidationResult(na verdade ValidationResult<boolean>devido ao parâmetro padrão) é equivalente à união produzida na resposta de bugs ou na resposta de CertainPerformance e pode ser usada da mesma maneira.

A vantagem aqui é que você também pode repassar um ValidationResult<false>valor conhecido e não precisará testar isValidcomo seria conhecido falsee errorStringexiste. Provavelmente não é necessário para um caso como esse - e tipos condicionais podem ser complexos e difíceis de depurar, portanto, provavelmente não devem ser usados ​​desnecessariamente. Mas você poderia, e isso parecia digno de menção.

KRyan
fonte
3
Tenho certeza de que elas são realmente úteis às vezes. Mas, para mim, a sintaxe parece realmente desagradável.
Peilonrayz
3
@Peilonrayz Eh, ele tem boa consistência com outras codificações Typescript e extendsé o operador certo para usar. E tem extremamente poderosa, especialmente desde que você também pode usá-lo para cavar tipos: type SecondOf<T> = T extends Pair<any, infer U> ? U : never;.
KRyan
@ Kryan Eu estive pensando sobre isso. Isso significa basicamente apenas SecondOf<number>'expande' para Pair<any, number>? Eu acho que o ditado "não julgue um livro pela capa" é pertinente aqui.
Peilonrayz 07/12/19
@Peilonrayz Ah, não; antes pelo contrário. SecondOf<Pair<any, number>>avalia como number. SecondOf<number>avalia como never, porque number extends Pair<any, infer U>é falso, como numbernão se estende a #Pair
KRyan