Um conditional type escolhe entre dois tipos com um teste que se lê como o operador ternário do JavaScript: T extends U ? X : Y. Se T pode ser atribuído a U, o resultado é X; senão, é Y.
Aqui extends significa "pode ser atribuído a", a mesma relação que o compilador usa quando você atribui um valor a uma variável. Conditional types existem só em tempo de compilação; eles são apagados da saída JavaScript.
A sintaxe
type Result = CheckedType extends TestType ? TrueType : FalseType;
Conditional types ficam úteis com generics, em que o tipo verificado é um type parameter que recebe um tipo concreto depois. No ramo verdadeiro, o TypeScript sabe que o tipo verificado corresponde ao teste, então o T["message"] acima é permitido, mesmo que um T simples não tenha propriedade message.
Eles também se aninham, como ternários encadeados:
O as TypeName<T> no corpo da função não é opcional, como mostra a próxima seção.
Tipos de retorno condicionais precisam de uma assertion
Uma função cujo tipo de retorno é um conditional type sobre o próprio type parameter não consegue retornar diretamente nenhum dos ramos. O TypeScript não estreita T dentro do corpo, então não consegue saber qual ramo vale:
index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.
Duas soluções comuns: overloads, que declaram cada par de entrada e saída e verificam quem chama com precisão, ou uma assertion na implementação.
Os overloads são tratados na página sobre sobrecarga de funções. Com uma assertion, o compilador confia em você: um ramo errado no corpo não seria detectado.
Conditional types distributivos
Quando o tipo verificado é um type parameter sozinho e recebe uma union, a condição roda uma vez para cada membro e os resultados são juntados em uma nova union:
A distribuição é o que faz Exclude e Extract funcionarem. Exclude<T, U> é definido como T extends U ? never : T: cada membro que corresponde a U vira never, e never desaparece de uma union. Então Exclude<"a" | "b" | "c", "a"> é "b" | "c".
Duas surpresas vêm da mesma regra. boolean é a union true | false, então ToArray<boolean> é false[] | true[], e não boolean[]. E never é a union vazia, então um conditional type distributivo que recebe never retorna never sem testar nada:
type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true
Extraindo tipos com infer
O infer declara uma nova variável de tipo dentro da cláusula extends. Se a correspondência dá certo, o TypeScript preenche essa variável a partir do tipo verificado, e você pode usá-la no ramo verdadeiro:
Leia T extends Promise<infer V> ? V : T como "se T é uma promise de alguma coisa, chame essa coisa de V e a retorne; senão, retorne T sem mudanças". O infer só é permitido na cláusula extends de um conditional type.
Uma variável infer pode ter a própria restrição com extends. A correspondência então só dá certo se o tipo inferido couber nela:
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string
Montando o seu próprio ReturnType
O ReturnType nativo é um conditional type de uma linha com infer. Escrevê-lo você mesmo é o exercício clássico que faz as duas ideias se encaixarem:
typeof makeUser transforma o valor da função no tipo dela, e então o conditional type o compara com "qualquer função" e captura o tipo de retorno como R. A versão da biblioteca padrão difere em dois detalhes: o parâmetro dela é restrito a tipos de função (T extends (...args: any) => any), então ReturnType<string> é erro de compilação em vez de never, e o ramo falso dela é any. A página sobre ReturnType trata também de Parameters, InstanceType e Awaited.
Conditional types recursivos
Um conditional type pode referenciar a si mesmo, o que permite desembrulhar qualquer profundidade de aninhamento:
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
O Awaited<T> nativo funciona assim, desembrulhando Promise<Promise<T>> até T. Na prática, mantenha a recursão rasa: recursão muito profunda ou sem limite faz o compilador desistir com error TS2589: Type instantiation is excessively deep and possibly infinite.
Referência rápida
| Padrão | Significado |
|---|---|
T extends U ? X : Y | X se T pode ser atribuído a U, senão Y |
T extends U ? never : T | remove os membros que correspondem a U (isto é o Exclude) |
T extends U ? T : never | mantém os membros que correspondem a U (isto é o Extract) |
[T] extends [U] ? X : Y | o mesmo teste, sem distribuir sobre uma union |
T extends (infer E)[] ? E : T | tipo do elemento de um array |
T extends Promise<infer V> ? V : T | tipo do valor de uma promise |
T extends (...args: any[]) => infer R ? R : never | tipo de retorno de uma função |
T extends [infer H, ...infer Rest] ? ... | primeiro elemento e o resto de uma tupla |
Perguntas frequentes
O que é um conditional type no TypeScript?
Um tipo no formato T extends U ? X : Y. Se T pode ser atribuído a U, o resultado é X; senão, Y. É um if/else para tipos, avaliado em tempo de compilação; nada dele existe no JavaScript gerado.
O que a palavra-chave infer faz no TypeScript?
O infer declara uma variável de tipo dentro da cláusula extends de um conditional type e deixa o TypeScript preenchê-la a partir do tipo que correspondeu. T extends Promise<infer V> ? V : T extrai de um tipo promise o tipo do valor resolvido. Ele só pode ser usado na cláusula extends de um conditional type.
O que é um conditional type distributivo?
Quando o tipo verificado é um type parameter sozinho e você passa uma union, a condição é aplicada a cada membro separadamente e os resultados são juntados. ToArray<string | number>, com type ToArray<T> = T extends unknown ? T[] : never, vira string[] | number[]. Envolva os dois lados em colchetes, [T] extends [unknown], para desligar isso.
Como obter o tipo de retorno de uma função no TypeScript?
Use o nativo ReturnType<typeof fn>. Ele é um conditional type com infer: T extends (...args: any) => infer R ? R : any. Para funções async, envolva-o em Awaited<...> para obter o valor resolvido em vez da promise.
Por que IsNever<never> retorna never em vez de true?
never é a union vazia, e um conditional type distributivo percorre os membros de uma union. Sem membros, não há nada a percorrer e o resultado é never. Escreva [T] extends [never] ? true : false para testar o próprio never.