Identificação
- Duração: 2 horas
- Tipo: teoria aplicada e laboratório
- Entrega: biblioteca genérica de resultados
- Laboratório:
../exemplos/aula-3.3/index.html
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:
- criar unions;
- diferenciar union e array;
- utilizar literal unions;
- estreitar tipos com
typeof; - utilizar igualdade,
ineinstanceof; - criar type guards;
- modelar discriminated unions;
- verificar exaustividade com
never; - criar funções genéricas;
- aplicar constraints;
- diferenciar generic, union e
any; - 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:
successpermite acessardata;errorpermite acessarmessage;emptynã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
anye 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,ineinstanceof. - 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;
neverajuda 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.