Engenharia de IA Aplicada à Programação Web
Aula 3 de 6
Navegar por módulos e aulas
  1. 01 Fundamentos da Web
  2. 02 JavaScript moderno
  3. 03 TypeScript, Git e qualidade
  4. 04 Angular
  5. 05 Node.js, NestJS e APIs
  1. 3.1 Introdução ao TypeScript
  2. 3.2 Interfaces, aliases e contratos
  3. 3.3 Unions, narrowing e generics
  4. 3.4 Configuração, lint e formatação
  5. 3.5 Git, branches e pull requests
  6. 3.6 Testes, documentação e projeto

Módulo 3 · Aula 3.3

Unions, narrowing e generics

2 horas Teoria + laboratório TypeScript, Git e qualidade
Ver fonte Markdown

Identificação

Introdução

O que é uma union?

Uma union descreve um valor que pode pertencer a mais de um tipo:

let identifier: string | number;

identifier = "task-001";
identifier = 42;

O símbolo | significa “ou”. Enquanto TypeScript não souber qual alternativa está presente, somente operações seguras para todas elas ficam disponíveis.

Union é a mesma coisa que array?

Não:

string | number

Representa um único valor que pode ser string ou número.

(string | number)[]

Representa um array cujos itens podem ser strings ou números.

O que é uma literal union?

type TaskStatus = "todo" | "doing" | "done";

Ela limita uma string às alternativas conhecidas e permite autocomplete e verificação de exaustividade.

O que é narrowing?

Narrowing, ou estreitamento, é o processo de verificar qual alternativa existe antes de utilizá-la:

function formatIdentifier(value: string | number): string {
  if (typeof value === "number") {
    return value.toFixed(0);
  }

  return value.toUpperCase();
}

Dentro do if, value é number. No caminho restante, é string.

O que é um generic?

Um generic é um parâmetro de tipo:

function first<T>(values: T[]): T | undefined {
  return values[0];
}

T representa o tipo dos itens naquela chamada:

first([1, 2]); // number | undefined
first(["A", "B"]); // string | undefined

O generic preserva a relação entre entrada e saída.

Generic é igual a any?

Não:

function firstAny(values: any[]): any {
  return values[0];
}

any perde a relação e desativa verificações. O generic mantém o tipo concreto fornecido ou inferido.

Onde isso aparece no projeto?

O laboratório modelará os estados de uma operação:

type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "empty" }
  | { status: "error"; message: string };

O campo status permitirá narrowing seguro, e T permitirá reutilizar a mesma estrutura com tarefas ou outros dados.

Objetivos

Ao final da aula, o aluno deverá conseguir:

  1. criar unions;
  2. diferenciar union e array;
  3. utilizar literal unions;
  4. estreitar tipos com typeof;
  5. utilizar igualdade, in e instanceof;
  6. criar type guards;
  7. modelar discriminated unions;
  8. verificar exaustividade com never;
  9. criar funções genéricas;
  10. aplicar constraints;
  11. diferenciar generic, union e any;
  12. modelar estados impossíveis de representar.

Pré-requisitos

  • Aulas 3.1 e 3.2 concluídas;
  • funções, objetos, interfaces e aliases;
  • tratamento de erros e estados assíncronos.

Pergunta orientadora

Como representar alternativas reais do sistema e preservar relações de tipos sem abandonar a segurança?

Roteiro sugerido

Etapa Duração
Unions e tipos literais 25 min
Técnicas de narrowing 30 min
Discriminated unions 25 min
Generics e constraints 25 min
Laboratório 10 min
Revisão 5 min

1. Union de primitivas

function printId(id: string | number): void {
  console.log(id);
}

Não podemos chamar diretamente:

id.toUpperCase();

O método não existe em number. Primeiro estreitamos.

2. Union de literais

type Priority =
  | "low"
  | "planned"
  | "high"
  | "critical";

Vantagens:

  • impede textos fora do domínio;
  • melhora autocomplete;
  • permite mapas completos;
  • facilita branches exaustivos.

3. Union de objetos

type SearchResult =
  | { found: true; value: string }
  | { found: false; reason: string };

Quando found é verdadeiro, value existe. Quando falso, reason existe.

Isso é melhor que:

type SearchResult = {
  found: boolean;
  value?: string;
  reason?: string;
};

A segunda forma permite estados contraditórios, como found: true sem value.

4. Narrowing com typeof

function normalize(value: string | number): string {
  if (typeof value === "number") {
    return String(value);
  }

  return value.trim();
}

typeof null é "object", uma particularidade histórica. Ao verificar objetos, confirme também value !== null.

5. Narrowing por igualdade

function renderStatus(
  status: "idle" | "loading" | "done",
): string {
  if (status === "loading") {
    return "Carregando...";
  }

  return status === "done" ? "Concluído" : "Aguardando";
}

Comparações removem alternativas incompatíveis.

6. Truthiness e seus riscos

function printLength(value: string | null): number {
  if (value) {
    return value.length;
  }

  return 0;
}

String vazia cai no caminho falso. Isso pode ser correto ou pode esconder um caso que deveria ser tratado separadamente.

Prefira checagem explícita quando vazio é diferente de ausente:

if (value !== null) {
}

7. Operador in

type Success = {
  data: string[];
};

type Failure = {
  error: string;
};

function render(result: Success | Failure): string {
  if ("data" in result) {
    return `${result.data.length} itens`;
  }

  return result.error;
}

Use quando uma propriedade distingue as alternativas.

8. instanceof

function formatDate(value: Date | string): string {
  if (value instanceof Date) {
    return value.toISOString();
  }

  return value;
}

instanceof verifica a cadeia de protótipos em ambiente de execução. Interfaces não podem ser usadas com instanceof, pois são apagadas.

9. Type predicate

function isTaskStatus(
  value: unknown,
): value is TaskStatus {
  return (
    typeof value === "string" &&
    ["todo", "doing", "done"].includes(value)
  );
}

value is TaskStatus informa que um retorno true estreita o argumento.

O compilador confia no predicate. Uma implementação incorreta cria falsa segurança.

10. Assertion function

function assertTaskStatus(
  value: unknown,
): asserts value is TaskStatus {
  if (!isTaskStatus(value)) {
    throw new Error("Status inválido");
  }
}

Após chamar, o fluxo considera o valor TaskStatus, desde que a função não lance.

11. Discriminated union

type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

Cada membro possui a mesma propriedade discriminante status, com literal diferente.

Uso:

function getMessage<T>(state: LoadState<T>): string {
  switch (state.status) {
    case "idle":
      return "Aguardando";
    case "loading":
      return "Carregando";
    case "success":
      return "Concluído";
    case "error":
      return state.message;
  }
}

12. Estados impossíveis

Modelo frágil:

interface RequestState<T> {
  loading: boolean;
  data?: T;
  error?: string;
}

Ele permite:

{
  loading: true,
  data: value,
  error: "Falhou"
}

Uma discriminated union define combinações válidas e impede alternativas contraditórias.

13. Exaustividade com never

function assertNever(value: never): never {
  throw new Error(
    `Estado não tratado: ${JSON.stringify(value)}`,
  );
}

No default:

default:
  return assertNever(state);

Se um novo membro for adicionado à union sem um novo case, TypeScript aponta que state não é never.

14. Parâmetro genérico

function identity<T>(value: T): T {
  return value;
}

Chamada explícita:

identity<string>("texto");

Inferida:

identity("texto");

Prefira inferência quando ela mantém o tipo desejado.

15. Relação entre entrada e saída

function first<T>(values: readonly T[]): T | undefined {
  return values[0];
}

O retorno depende do tipo dos itens. Uma union fixa perderia parte dessa relação:

function first(
  values: (string | number)[],
): string | number | undefined {
}

O generic funciona também para Task, User ou qualquer outro tipo.

16. Nomes de parâmetros genéricos

Nomes comuns:

  • T: type;
  • K: key;
  • V: value;
  • E: error ou element, conforme contexto;
  • nomes descritivos: TData, TError, TItem.

Em tipos complexos, nomes descritivos ajudam:

type Result<TData, TError> =
  | { ok: true; data: TData }
  | { ok: false; error: TError };

17. Generic com múltiplos parâmetros

function createPair<TLeft, TRight>(
  left: TLeft,
  right: TRight,
): [TLeft, TRight] {
  return [left, right];
}

Retorno:

const pair = createPair("task", 1);
// [string, number]

18. Constraints

function getId<T extends { id: number }>(
  value: T,
): number {
  return value.id;
}

T pode ser qualquer estrutura que possua id: number. O retorno preserva o tipo concreto quando necessário.

19. keyof

function getProperty<T, K extends keyof T>(
  object: T,
  key: K,
): T[K] {
  return object[key];
}

key só pode ser uma chave do objeto, e o retorno corresponde ao tipo daquela propriedade.

getProperty(task, "title"); // string
getProperty(task, "missing"); // erro

20. Valores padrão em generics

type ApiResult<TData, TError = string> =
  | { ok: true; data: TData }
  | { ok: false; error: TError };

Se TError não for informado, será string.

21. Generic em interface

interface Page<TItem> {
  items: TItem[];
  total: number;
  page: number;
}

type TaskPage = Page<Task>;

Uma estrutura de paginação pode ser reutilizada para diferentes entidades.

22. Generics não devem ser decorativos

Este generic não cria relação útil:

function logValue<T>(value: T): void {
  console.log(value);
}

Poderia ser:

function logValue(value: unknown): void {
  console.log(value);
}

Use generic quando o parâmetro aparece em mais de um ponto relevante ou quando preserva informação para o consumidor.

23. Generic ou union?

Use union quando as alternativas fazem parte do domínio:

type Status = "todo" | "done";

Use generic quando a estrutura funciona com diferentes tipos mantendo relações:

type Page<T> = {
  items: T[];
};

Eles podem trabalhar juntos:

type Result<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

24. Biblioteca do laboratório

type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "empty" }
  | { status: "error"; message: string };

A mesma estrutura pode ser:

LoadState<Task[]>
LoadState<User[]>
LoadState<Course>

25. Laboratório guiado

Abra a biblioteca de estados.

Etapa 1 — Alterne cenários

Execute sucesso, vazio e erro. Observe os campos disponíveis em cada estado.

Etapa 2 — Acompanhe o narrowing

O painel mostra qual case do switch foi executado:

  • success permite acessar data;
  • error permite acessar message;
  • empty não contém nenhum dos dois.

Etapa 3 — Compare payloads

O exemplo usa LoadState<Task[]>, mas a biblioteca não conhece Task internamente. O generic preserva o payload.

Etapa 4 — Examine a exaustividade

Adicione um estado "cancelled" à fonte TypeScript sem criar um case. Observe o erro esperado no assertNever.

26. Erros comuns

Usar propriedade sem narrowing

state.data;

data não existe em todos os membros.

Criar type guard que sempre retorna true

O compilador confia no predicate; a implementação precisa validar de verdade.

Usar as no lugar de narrowing

Assertions silenciam o compilador, não provam a alternativa.

Transformar tudo em generic

Um generic sem relação útil aumenta complexidade.

Esquecer retorno possivelmente undefined

first([]) não possui item. O contrato precisa representar isso.

27. Boas práticas

  • use literal unions para alternativas fechadas;
  • escolha um discriminante claro;
  • modele estados válidos, não combinações de flags;
  • faça narrowing por verificações reais;
  • implemente type guards com cuidado;
  • use exaustividade;
  • crie generics para preservar relações;
  • aplique constraints mínimas;
  • prefira inferência;
  • evite any e assertions desnecessárias.

28. Exercícios

Exercício 1 — Union

Crie Identifier = string | number e formate com typeof.

Exercício 2 — Resultado

Modele sucesso e falha com discriminante ok.

Exercício 3 — Type guard

Crie isTaskStatus(value: unknown).

Exercício 4 — Generic

Crie last<T>(values: readonly T[]): T | undefined.

Exercício 5 — Constraint

Crie uma função que aceita qualquer objeto com id.

29. Desafio

Modele:

  • Result<TData, TError>;
  • Page<TItem>;
  • LoadState<T>;
  • renderização exaustiva;
  • validador de unknown;
  • função getProperty<T, K extends keyof T>.

Inclua um teste de compilação esperado para estado não tratado.

30. Lista de verificação de conclusão

  • Sei diferenciar union e array.
  • Uso literal unions para domínio fechado.
  • Consigo estreitar com typeof, in e instanceof.
  • Sei criar um type predicate.
  • Modelo estados com discriminated union.
  • Verifico exaustividade com never.
  • Entendo a relação preservada por generic.
  • Sei aplicar constraint.
  • Diferencio generic, union e any.
  • Não uso assertion no lugar de prova.

31. Critérios de avaliação

Critério Pontos
Modelagem das unions 20
Narrowing correto 20
Estado discriminado 20
Generic e relação de tipos 20
Exaustividade 10
Validação em ambiente de execução 10
Total 100

Resumo

Nesta aula, aprendemos que:

  • unions representam alternativas;
  • narrowing prova qual alternativa está presente;
  • discriminated unions impedem estados contraditórios;
  • never ajuda a verificar exaustividade;
  • generics preservam relações entre tipos;
  • constraints definem requisitos mínimos;
  • generic não é any;
  • type guards precisam corresponder à validação real.

Próxima aula

Na Aula 3.4, configuraremos o compilador em modo estrito e criaremos um fluxo de qualidade com lint, formatação e comandos reproduzíveis.