Menu

Conditional types e infer em TypeScript, com exemplos

Um conditional type escolhe um de dois tipos com base em um teste: T extends U ? X : Y. Veja a sintaxe, como os conditional types se distribuem sobre unions (e como impedir isso), como extrair tipos com infer e como escrever o seu próprio ReturnType.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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ãoSignificado
T extends U ? X : YX se T pode ser atribuído a U, senão Y
T extends U ? never : Tremove os membros que correspondem a U (isto é o Exclude)
T extends U ? T : nevermantém os membros que correspondem a U (isto é o Extract)
[T] extends [U] ? X : Yo mesmo teste, sem distribuir sobre uma union
T extends (infer E)[] ? E : Ttipo do elemento de um array
T extends Promise<infer V> ? V : Ttipo do valor de uma promise
T extends (...args: any[]) => infer R ? R : nevertipo 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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR