TypeScript: Genéricos avançados do TypeScript

Última atualização: 2026-08-26

Agora que você já domina os conceitos básicos dos genéricos, esta lição passa a abordar tópicos mais avançados — tipos condicionais, infer e tipos mapeados. Essas são ferramentas essenciais para a programação baseada em tipos no TypeScript e constituem a base para a compreensão do código-fonte dos tipos utilitários embutidos.

1. Tipos condicionais

Os tipos condicionais selecionam resultados diferentes com base em condições de tipo — de forma semelhante ao operador ternário no nível do tipo:

(1) Sintaxe básica

TYPESCRIPT
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;   // "yes"
type B = IsString<number>;   // "no"
type C = IsString<"hello">;  // "yes" ("hello" is a string subtype of)

(2) Tipos condicionais e tipos de união (distribuídos)

Quando T é um tipo de união, o tipo condicional avalia cada membro separadamente, de maneira “distribuída”:

TYPESCRIPT
type ToString<T> = T extends string ? "string" : "other";

// T = string | number → Evaluate each one separately → "string" | "other"
type Result = ToString<string | number>;  // "string" | "other"

(3) Desativar o comportamento distribuído

Coloque o código entre [T] para desativar a avaliação distribuída:

TYPESCRIPT
type IsNever<T> = [T] extends [never] ? "yes" : "no";

type A = IsNever<never>;           // "yes"
type B = IsNever<string | never>;  // "no"(No longer available)

▶ Exemplo: Aplanamento de matriz com segurança de tipos

TYPESCRIPT
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;        // string
type B = Flatten<number>;          // number
type C = Flatten<boolean[]>;       // boolean
▶ Experimente

Saída:

TEXT 📖 Somente leitura
// Executed successfully


2. A palavra-chave infer

infer “Inferir” uma variável de tipo em uma condição de tipo — essa é uma das ferramentas mais poderosas da programação de tipos no TypeScript:

(1) Inferindo o tipo dos elementos de uma matriz

TYPESCRIPT
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type A = ArrayElement<string[]>;   // string
type B = ArrayElement<number[]>;   // number
type C = ArrayElement<string>;     // never(string Not an array)

(2) Inferindo o tipo de retorno de uma função

TYPESCRIPT
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = GetReturnType<() => string>;          // string
type B = GetReturnType<(x: number) => boolean>; // boolean
type C = GetReturnType<(x: string) => void>;    // void
type D = GetReturnType<string>;                  // never(string Not a function)

(3) Inferência dos tipos dos parâmetros de função

TYPESCRIPT
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;

type A = GetParameters<(a: string, b: number) => void>;  // [string, number]
type B = GetParameters<() => void>;                        // []
type C = GetParameters<(x: boolean) => string>;            // [boolean]

(4) Inferindo o tipo do valor resolvido de uma promessa

TYPESCRIPT
type Awaited<T> = T extends Promise<infer U> ? U : T;

type A = Awaited<Promise<string>>;     // string
type B = Awaited<Promise<number[]>>;   // number[]
type C = Awaited<string>;              // string(No Promise,Return directly)

// nested Promise——Recursive Unpacking
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;

type D = DeepAwaited<Promise<Promise<string>>>;  // string


3. Tipos mapeados

Tipos de mapeamento: Como criar um novo tipo com base em um antigo — aplicando transformações a cada propriedade:

(1) Sintaxe básica

TYPESCRIPT
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
}

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

type OptionalUser = Optional<User>;
// { name?: string; age?: number; email?: string }

(2) Modificadores de mapeamento

Use +/- para adicionar ou remover modificadores:

TYPESCRIPT
// Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// Remove (optional)(?)
type Required2<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  readonly host: string;
  readonly port: number;
  debug?: boolean;
}

type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }  —— readonly Removed

type RequiredConfig = Required2<Config>;
// { readonly host: string; readonly port: number; debug: boolean }  —— ? Removed

(3) Remapeamento de teclas (TypeScript 4.1+)

TYPESCRIPT
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

(4) Propriedades do filtro

TYPESCRIPT
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }  —— Keep only string Type Properties

▶ Exemplo: Como criar um cliente de API com segurança de tipos

TYPESCRIPT
// Definition API Routing Type
type ApiRoutes = {
  "/users": { response: { id: number; name: string }[] };
  "/users/:id": { response: { id: number; name: string }; params: { id: number } };
  "/posts": { response: { id: number; title: string }[] };
};

// Extract Response Type Based on Route
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];

type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]

type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }

// Type-safe fetch Function
function fetchApi<R extends keyof ApiRoutes>(
  route: R,
  ...args: "params" extends keyof ApiRoutes[R]
    ? [params: ApiRoutes[R]["params"]]
    : []
): Promise<ApiResponse<R>> {
  // Implementation Omitted
  return {} as any;
}

// Automatically infer parameters and return types at runtime
// let users = fetchApi("/users");           // No parameters required
// let user = fetchApi("/users/:id", { id: 1 });  // Must be provided params
▶ Experimente

Saída:

TEXT 📖 Somente leitura
// Executed successfully


4. Valores padrão para genéricos

Os parâmetros de tipo genérico podem ter valores padrão — o tipo padrão é usado quando o tipo não pode ser inferido ou não precisa ser especificado:

TYPESCRIPT
interface PaginatedResult<T, PageSize = 10> {
  data: T[];
  total: number;
  pageSize: PageSize;
}

// Use the default values
type UserResult = PaginatedResult<User>;
// data: User[]; total: number; pageSize: 10

// Override the default value
type CustomResult = PaginatedResult<User, 20>;
// data: User[]; total: number; pageSize: 20

(1) Restrições aos valores padrão

Os parâmetros de tipo com valores padrão devem vir após os parâmetros sem valores padrão:

TYPESCRIPT
// ✅ Correct
type A<T, U = string> = { first: T; second: U };

// ❌ Error——Those with default values U When there is no default value, T Previous
// type B<U = string, T> = { first: T; second: U };


5. Covariância e invariância

Esse é um dos conceitos avançados mais confusos do sistema de tipos do TypeScript — é preciso entendê-lo para compreender por que algumas atribuições são válidas e outras não.

(1) Covariável

"Se A é um subtipo de B, então Container<A> também é um subtipo de Container<B>" — Mudanças na mesma direção:

TYPESCRIPT
// string is a subtype of string | number
// string[] Me too (string | number)[] subtypes of → Covariation
let strings: string[] = ["a", "b"];
let mixed: (string | number)[] = strings;  // ✅ Covariant Safety

(2) Adjunto (contravariável)

Os tipos de parâmetros de função são inversos — “Se A é um subtipo de B, então (B => void) é um subtipo de (A => void)” — a relação funciona na direção oposta:

TYPESCRIPT
// string is a subtype of string | number
// (string | number => void) is a subtype of (string => void) → Inversion

type StringHandler = (arg: string) => void;
type MixedHandler = (arg: string | number) => void;

let mixedHandler: MixedHandler = (arg) => console.log(arg);
let stringHandler: StringHandler = mixedHandler;  // ✅ Inversion——Functions that handle broader types can be assigned to variables that handle narrower types.

(3) Por que os argumentos da função estão invertidos?

TYPESCRIPT
// If the parameter is covariant——Unsafe
let dogHandler: (dog: Dog) => void = (dog) => dog.bark();
let animalHandler: (animal: Animal) => void = dogHandler;  // ❌ Danger!
// Call animalHandler(cat) → dog.bark() called on cat → Runtime Error

// Inverting is the safe option——Functions that handle broader types can safely handle narrower types.
💡 Conclusão rápida: Os valores de retorno das funções são covariantes (segurança de tipo de retorno mais específica), enquanto os parâmetros das funções são contravariantes (segurança de tipo de parâmetro mais geral). O TypeScript aplica rigorosamente as verificações contravariantes no modo strictFunctionTypes.



▶ Exemplo: Construindo um cliente de API type-safe

Saída:

TEXT 📖 Somente leitura
42
TYPESCRIPT
// Definition API Routing Type
type ApiRoutes = {
  "/users": { response: { id: number; name: string }[] };
  "/users/:id": { response: { id: number; name: string }; params: { id: number } };
  "/posts": { response: { id: number; title: string }[] };
};

// Extract Response Type Based on Route
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];

type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]

type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }

// Type-safe fetch Function
function fetchApi<R extends keyof ApiRoutes>(
  route: R,
  ...args: "params" extends keyof ApiRoutes[R]
    ? [params: ApiRoutes[R]["params"]]
    : []
): Promise<ApiResponse<R>> {
  // Implementation Omitted
  return {} as any;
}

// Automatically infer parameters and return types at runtime
  // let users = fetchApi("/users");           // No parameters required
  // let user = fetchApi("/users/:id", { id: 1 });  // Must be provided params

Saída:

TEXT 📖 Somente leitura
Type-safe API client — no runtime output (compile-time type checking only)


❓ Perguntas Frequentes

P: Qual é a diferença entre infer e o genérico T? R: O genérico T é um parâmetro de tipo fornecido pelo chamador, enquanto infer é uma variável de tipo inferida automaticamente dentro de um tipo condicional. T significa “você me diz”, e infer significa “eu mesmo infiro”. infer só pode ser usado na cláusula extends de um tipo condicional.

P: Qual é a relação entre tipos de mapeamento e tipos utilitários (como Partial e Required)? R: Os tipos utilitários são implementados por meio de tipos de mapeamento. Partial, Required, Readonly, Pick, Omit e outros são todos tipos de mapeamento embutidos no TypeScript. Compreender como os tipos de mapeamento funcionam ajudará você a entender como esses tipos utilitários operam, e você também poderá criar seus próprios tipos utilitários personalizados.

P: A covariância e a contravariância são importantes no desenvolvimento do dia a dia? R: Na maioria das vezes, você não precisa se preocupar com elas — a verificação de tipos do TypeScript lida com elas automaticamente. Você só precisa entendê-las ao escrever funções genéricas, funções de ordem superior ou ferramentas de tipos. Iniciantes devem apenas se familiarizar com os conceitos e se aprofundar neles somente quando encontrarem incompatibilidades de tipos em atribuições de funções.

P: Quando um tipo condicional é “distribuído”? R: Quando o T em um tipo condicional é um parâmetro de tipo simples (não envolto em uma tupla, objeto etc.) e é um tipo de união, a avaliação é distribuída. Envolvê-lo em [T] extends [U] desativa a distribuição. O exemplo IsNever é o cenário mais comum em que a distribuição precisa ser desativada — never é ignorado durante a distribuição quando é um membro da união.

📖 Resumo

📝 Exercícios

  1. Problema básico (Dificuldade ⭐): Implemente IsArray<T> usando tipos condicionais — se T for um tipo de matriz, retorne true; caso contrário, retorne false. Teste IsArray<string[]> e IsArray<number>.
  2. Problema avançado (Dificuldade ⭐⭐): Implemente Stringify<T> usando um tipo de mapeamento — converta os tipos de todas as propriedades de um objeto para string. Por exemplo, { age: number }{ age: string }.
  3. Problema de desafio (Dificuldade ⭐⭐⭐): Use infer e tipos condicionais para implementar DeepPromise<T> — desembrulhe recursivamente Promises aninhadas até obter um tipo que não seja Promise. O caso de teste DeepPromise<Promise<Promise<Promise<number>>>> deve retornar um número.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%