Aula 3: TypeScript para Backend e Frontend
Nas duas primeiras aulas vimos o que vamos construir: um serviço e dois clientes, com as decisões de arquitetura que essa divisão implica. Esta aula trata de com o que vamos construir — a linguagem comum ao backend em NestJS e ao frontend web em Next.js.
É a última aula do Módulo 1 dedicada a fundamentos antes de escrever a primeira linha do serviço. Nada aqui é específico de framework: é a base de linguagem que os Módulos 2 e 3 assumem como dada.
Esta aula é mais extensa que as demais do módulo, porque acumula duas funções: conduzir a aula e servir de consulta ao longo do semestre. Não é preciso reter tudo de uma vez — os laboratórios ao final fixam o essencial, e o restante você volta a consultar quando precisar.
Objetivos
Ao final desta aula, você deve ser capaz de:
- Explicar a relação entre JavaScript, TypeScript e o runtime Node.js, e por que a tipagem estática é adotada nesta disciplina.
- Utilizar os recursos de JavaScript moderno (ES2020+) usados no dia a dia: módulos, funções de seta, desestruturação, métodos de array e
async/await. - Declarar tipos, interfaces, uniões e tipos literais, aplicando narrowing para escrever código seguro em relação a
nulleundefined. - Aplicar generics e utility types para modelar contratos reutilizáveis, como DTOs e respostas de API.
- Reconhecer classes, parameter properties e decorators como a base da injeção de dependência do NestJS.
- Configurar um projeto TypeScript, compreendendo as opções de
tsconfig.jsonque mais afetam a qualidade do código. - Executar os laboratórios práticos no TypeScript Playground e em um projeto local, diagnosticando erros de compilação e distinguindo-os de erros em tempo de execução.
Contexto: por que TypeScript nesta disciplina
O navegador executa JavaScript. O Node.js, que roda o backend, também executa JavaScript. Nenhum dos dois executa TypeScript diretamente. Então por que escrever TypeScript?
Porque JavaScript é uma linguagem de tipagem dinâmica: o tipo de um valor só
é conhecido quando o programa está rodando. Um erro banal — passar uma string
onde se esperava um número, ler uma propriedade que não existe, esquecer de
tratar um undefined — só aparece em produção, no meio de uma requisição.
TypeScript é JavaScript acrescido de um sistema de tipos estático. O código TypeScript é verificado pelo compilador e depois transpilado para JavaScript comum, que é o que efetivamente executa. Os tipos não existem em tempo de execução: eles são uma ferramenta de projeto e de verificação.
Para quem vem de Java ou C#, a mudança de perspectiva é a seguinte:
| Aspecto | Java | TypeScript |
|---|---|---|
| Verificação de tipos | Em tempo de compilação, obrigatória | Em tempo de compilação, opcional e gradual |
| Tipos em tempo de execução | Existem (reflection, instanceof) | Não existem — são apagados na transpilação |
| Sistema de tipos | Nominal (compatibilidade por nome da classe) | Estrutural (compatibilidade por formato do objeto) |
| Unidade de compilação | Classe | Módulo (arquivo) |
| Herança | Classes e interfaces | Classes, interfaces e tipos compostos (união, interseção) |
Nesta disciplina, TypeScript aparece nos dois lados: NestJS é escrito em TypeScript e usa decorators intensivamente; Next.js e React se beneficiam da tipagem de props, estado e respostas de API. É essa continuidade que permite, por exemplo, definir o formato de um recurso uma vez e reaproveitá-lo no cliente web.
Qual versão de TypeScript?
O ecossistema passou por uma transição grande em 2026, e vale saber em que ponto estamos:
| Versão | Quando | O que significa |
|---|---|---|
| 5.x | até 2025 | Base do que a maior parte dos tutoriais e projetos ainda usa |
| 6.0 | março de 2026 | Última versão escrita em TypeScript/JavaScript. Mudou vários padrões e marcou opções antigas como deprecated, preparando a transição |
| 7.0 | julho de 2026 | Reescrita do compilador em Go, entre 8 e 12 vezes mais rápida. Mantém a mesma verificação de tipos da 6.0, mas transforma as deprecations dela em erros |
Para o que você vai escrever nesta disciplina, a linguagem é a mesma: o sistema de tipos da 7.0 é estruturalmente idêntico ao da 6.0. O que muda são os padrões de configuração e a velocidade da verificação.
strictagora étruepor padrão. Código de tutoriais antigos, escrito sem anotações de tipo, pode não compilar mais. Isso é bom — mas explica erros inesperados ao copiar exemplos da internet.- A 7.0 ainda não expõe uma API programática (prevista para a 7.1). Por
isso, ferramentas que embutem o compilador — Vue, Svelte, Astro, MDX,
verificação de templates do Angular, alguns loaders de bundlers — ainda
dependem da 6.0. Na prática, muitos times usam a 7.0 no
tscda CI e mantêm a 6.0 no editor.
Os trechos desta aula são executáveis. Você tem duas opções, detalhadas mais adiante na seção Laboratórios Práticos:
- Sem instalar nada — TypeScript Playground, que compila no próprio navegador e mostra o JavaScript gerado e os erros lado a lado.
- Localmente — Node.js 20 ou superior, com TypeScript e um executor no projeto.
mkdir lab-ts && cd lab-ts
npm init -y
npm install -D typescript tsx
npx tsc --init
Parte 1 — JavaScript moderno essencial
TypeScript é um superconjunto de JavaScript: todo JavaScript válido é TypeScript válido. Portanto, antes dos tipos, é preciso dominar a linguagem base. Esta seção cobre o subconjunto de ES2020+ que aparece o tempo todo em NestJS e Next.js.
Os exemplos desta parte são JavaScript puro, sem anotações de tipo. Se você
colá-los em um arquivo .ts com strict ligado (o padrão desde a versão 6.0),
o compilador vai reclamar de parâmetros sem tipo — é o comportamento esperado.
Salve-os como .js, ou acrescente as anotações à medida que avançar na Parte 2.
Declaração de variáveis: const e let
const taxa = 0.15; // não pode ser reatribuída
let contador = 0; // pode ser reatribuída
contador = contador + 1;
// var existe, mas não deve ser usado: seu escopo é a função, não o bloco
Regra prática: use const por padrão; troque para let só quando precisar
reatribuir. Note que const impede a reatribuição, não a mutação:
const config = { porta: 3000 };
config.porta = 4000; // permitido: o objeto foi mutado, não reatribuído
// config = { porta: 5000 }; // erro: reatribuição de const
Funções e funções de seta
// declaração tradicional
function somar(a, b) {
return a + b;
}
// função de seta (arrow function)
const somar2 = (a, b) => {
return a + b;
};
// corpo de expressão: o return é implícito
const somar3 = (a, b) => a + b;
// parâmetro único dispensa parênteses
const dobrar = n => n * 2;
Funções são valores de primeira classe: podem ser passadas como argumento, retornadas e armazenadas em variáveis. É esse recurso que sustenta os métodos de array, os callbacks e os hooks do React.
this em funções de setaFunções de seta não têm this próprio: herdam o this do escopo em que
foram definidas. Isso resolve uma das maiores fontes de bugs em JavaScript
clássico, mas significa que uma função de seta não pode ser usada como método
que dependa de this dinâmico. Em classes de NestJS, prefira métodos comuns.
Template literals
const nome = 'Ana';
const idade = 20;
const msg = `Aluna ${nome}, ${idade} anos`;
const sql = `
SELECT *
FROM alunos
WHERE idade >= ${idade}
`;
Desestruturação
Extrai propriedades de objetos e elementos de arrays diretamente para variáveis. É onipresente no código React e NestJS.
const aluno = { id: 1, nome: 'Ana', curso: 'ADS', ativo: true };
// objeto
const { nome, curso } = aluno;
// com renomeação e valor padrão
const { nome: nomeAluno, turno = 'noite' } = aluno;
// array (por posição)
const cores = ['vermelho', 'verde', 'azul'];
const [primeira, , terceira] = cores;
// em parâmetros de função — padrão comum em componentes React
function saudar({ nome, curso }) {
return `Olá, ${nome} (${curso})`;
}
Spread e rest
O operador ... faz duas coisas conforme a posição: espalha (spread) ou
agrupa (rest).
// spread: copiar e sobrescrever — base da atualização imutável de estado
const original = { id: 1, nome: 'Ana', curso: 'ADS' };
const atualizado = { ...original, curso: 'BCC' };
// { id: 1, nome: 'Ana', curso: 'BCC' }
const listaA = [1, 2];
const listaB = [3, 4];
const juntas = [...listaA, ...listaB]; // [1, 2, 3, 4]
// rest: agrupar o restante
function somarTodos(...numeros) {
return numeros.reduce((acc, n) => acc + n, 0);
}
somarTodos(1, 2, 3, 4); // 10
const { id, ...semId } = original; // semId = { nome, curso }
O spread faz cópia rasa (shallow copy): objetos aninhados continuam
compartilhados entre a cópia e o original. Para estruturas aninhadas, copie nível
a nível ou use structuredClone(objeto).
Métodos de array
Processamento declarativo de coleções — equivalente conceitual à Streams API do Java.
const alunos = [
{ nome: 'Ana', nota: 8.5, ativo: true },
{ nome: 'Bruno', nota: 6.0, ativo: true },
{ nome: 'Célia', nota: 9.2, ativo: false },
];
alunos.filter(a => a.ativo); // seleciona
alunos.map(a => a.nome); // transforma
alunos.find(a => a.nome === 'Ana'); // primeiro que casa (ou undefined)
alunos.some(a => a.nota > 9); // existe algum? → true
alunos.every(a => a.nota >= 6); // todos? → true
alunos.reduce((acc, a) => acc + a.nota, 0); // agrega → 23.7
alunos.sort((a, b) => b.nota - a.nota); // ordena (muta o array!)
| Método | Retorna | Muta o array original |
|---|---|---|
map | Novo array transformado | Não |
filter | Novo array filtrado | Não |
find | Elemento ou undefined | Não |
reduce | Valor acumulado | Não |
some / every | boolean | Não |
sort / reverse | O próprio array | Sim |
push / splice | Depende | Sim |
sort() ordena no lugar e, sem comparador, converte os elementos para string
— [10, 9, 100].sort() produz [10, 100, 9]. Sempre forneça o comparador para
números.
Desde o ES2023 existem versões que não mutam o original e devolvem uma cópia:
toSorted(), toReversed(), toSpliced() e with(). Elas são a escolha certa
em React, onde mutar o array de estado impede a re-renderização.
const porNota = alunos.toSorted((a, b) => b.nota - a.nota); // alunos intacto
const semPrimeiro = alunos.toSpliced(0, 1);
const corrigido = alunos.with(0, { ...alunos[0], nota: 9.0 });
Encadeamento opcional e coalescência nula
const pedido = { cliente: { endereco: null } };
// sem optional chaining: quebra com TypeError
// const cidade = pedido.cliente.endereco.cidade;
const cidade = pedido.cliente?.endereco?.cidade; // undefined, sem erro
const cidadeOuPadrao = cidade ?? 'não informada'; // usa o padrão se null/undefined
?? não é |||| cai no valor padrão para qualquer valor falsy — inclusive 0, '' e
false. ?? só cai no padrão quando o valor é null ou undefined. Para
const desconto = valorInformado ?? 0, a diferença é decisiva.
Assincronismo: Promises e async/await
JavaScript é single-threaded: há uma única linha de execução. Operações de E/S (rede, disco, banco de dados) não bloqueiam essa linha — são delegadas ao ambiente e seus resultados chegam depois, via event loop. É por isso que quase tudo que envolve rede ou banco em Node é assíncrono.
Uma Promise representa um valor que ainda não está disponível. Pode estar pending, fulfilled (com um valor) ou rejected (com um erro).
- async/await (preferir)
- then/catch
async function buscarAluno(id) {
try {
const resposta = await fetch(`https://api.exemplo.com/alunos/${id}`);
if (!resposta.ok) {
throw new Error(`Falha HTTP ${resposta.status}`);
}
const aluno = await resposta.json();
return aluno;
} catch (erro) {
console.error('Erro ao buscar aluno:', erro);
throw erro;
}
}
// quem chama também precisa aguardar
const aluno = await buscarAluno(1);
function buscarAluno(id) {
return fetch(`https://api.exemplo.com/alunos/${id}`)
.then(resposta => {
if (!resposta.ok) throw new Error(`Falha HTTP ${resposta.status}`);
return resposta.json();
})
.catch(erro => {
console.error('Erro ao buscar aluno:', erro);
throw erro;
});
}
Regras importantes:
- Toda função marcada com
asyncretorna uma Promise, mesmo que retorne um valor simples. awaitsó pode ser usado dentro de funçõesasync(ou no topo de um módulo ESM).- Erros em código assíncrono se propagam como exceções quando se usa
await+try/catch.
Para operações independentes, execute em paralelo:
// sequencial: soma dos tempos
const aluno = await buscarAluno(1);
const notas = await buscarNotas(1);
// paralelo: tempo do mais lento
const [aluno2, notas2] = await Promise.all([buscarAluno(1), buscarNotas(1)]);
Módulos (ESM)
Cada arquivo é um módulo com escopo próprio. Nada é global por acidente.
// arquivo: aluno.service.ts
export const TAXA_APROVACAO = 6.0;
export function aprovado(nota: number) {
return nota >= TAXA_APROVACAO;
}
export default class AlunoService { /* ... */ }
// arquivo: main.ts
import AlunoService, { aprovado, TAXA_APROVACAO } from './aluno.service';
import * as tudo from './aluno.service';
O ecossistema Node ainda convive com dois sistemas de módulos: CommonJS
(require/module.exports, herança histórica do Node) e ESM
(import/export, o padrão da linguagem). Nesta disciplina escrevemos sempre
import/export; o compilador cuida da saída conforme a configuração do
projeto.
Parte 2 — O sistema de tipos
Anotação e inferência
let nome: string = 'Ana'; // anotação explícita
let idade = 20; // inferido como number
let ativo = true; // inferido como boolean
idade = 'vinte'; // erro: Type 'string' is not assignable to type 'number'
Boa prática: não anote o que o compilador já infere corretamente. Anote onde o tipo é um contrato: parâmetros de função, retornos públicos e estruturas de dados.
Tipos primitivos e especiais
| Tipo | Uso | Observação |
|---|---|---|
string, number, boolean | Primitivos básicos | number cobre inteiros e decimais (IEEE 754) |
bigint | Inteiros arbitrariamente grandes | Literal com sufixo n: 10n |
null / undefined | Ausência de valor | Com strictNullChecks, não são atribuíveis a outros tipos |
any | Desliga a verificação | Evitar — anula o benefício do TypeScript |
unknown | Valor de tipo desconhecido | Alternativa segura ao any: exige verificação antes do uso |
never | Nunca ocorre | Retorno de função que sempre lança erro; usado em verificações de exaustividade |
void | Função sem retorno útil | Diferente de undefined em posição de retorno |
function falhar(msg: string): never {
throw new Error(msg);
}
function processar(dado: unknown) {
// dado.length; // erro: objeto é de tipo desconhecido
if (typeof dado === 'string') {
return dado.length; // ok: o tipo foi estreitado para string
}
return 0;
}
Arrays, tuplas e objetos
const notas: number[] = [8.5, 6.0, 9.2];
const nomes: Array<string> = ['Ana', 'Bruno'];
// tupla: tamanho e tipos fixos por posição
const par: [string, number] = ['Ana', 20];
// objeto literal tipado
const aluno: { id: number; nome: string; email?: string } = {
id: 1,
nome: 'Ana',
};
interface e type
As duas formas de dar nome a um formato de dados.
- interface
- type alias
interface Aluno {
readonly id: number; // não pode ser alterado após a criação
nome: string;
email?: string; // propriedade opcional
curso: string;
}
interface AlunoComNotas extends Aluno {
notas: number[];
}
// interfaces podem ser reabertas (declaration merging)
interface Aluno {
matricula: string;
}
type Aluno = {
readonly id: number;
nome: string;
email?: string;
curso: string;
};
type AlunoComNotas = Aluno & { notas: number[] }; // interseção
// type também dá nome a uniões, tuplas, funções e primitivos
type ID = number | string;
type Comparador = (a: number, b: number) => number;
Quando usar cada um:
| Situação | Recomendação |
|---|---|
| Formato de um objeto ou contrato de classe | interface |
| União, interseção, tupla, tipo de função | type |
| Precisa ser estendido por bibliotecas externas | interface |
| Tipo derivado com utility types | type |
Na prática, os dois se sobrepõem bastante. O importante é ser consistente dentro do projeto.
Tipagem estrutural
TypeScript compara tipos pelo formato, não pelo nome. Se um objeto tem as propriedades exigidas, ele é compatível — mesmo sem declarar que "implementa" o tipo.
interface Ponto { x: number; y: number }
function distanciaOrigem(p: Ponto) {
return Math.sqrt(p.x ** 2 + p.y ** 2);
}
const posicao = { x: 3, y: 4, cor: 'azul' };
distanciaOrigem(posicao); // ok: tem x e y — o extra é ignorado
Isso é o oposto do sistema nominal do Java, em que a compatibilidade depende
de a classe declarar implements.
Se o objeto for escrito diretamente no ponto da chamada, o compilador é mais rigoroso e reclama de propriedades a mais:
distanciaOrigem({ x: 3, y: 4, cor: 'azul' });
// erro: 'cor' does not exist in type 'Ponto'
A regra existe para pegar erros de digitação em literais (heigth em vez de
height). Se o objeto vem de uma variável, o compilador assume que você sabe o
que está fazendo.
Uniões, tipos literais e narrowing
type Status = 'pendente' | 'aprovado' | 'rejeitado';
let situacao: Status = 'pendente';
// situacao = 'cancelado'; // erro: valor fora da união
function formatarId(id: number | string): string {
if (typeof id === 'number') {
return id.toFixed(0); // aqui id é number
}
return id.toUpperCase(); // aqui id só pode ser string
}
O compilador estreita (narrowing) o tipo conforme as verificações do
código: typeof, instanceof, in, comparação com literais e type guards
personalizados.
// união discriminada: o campo 'tipo' identifica a variante
type Resultado =
| { tipo: 'sucesso'; dados: Aluno[] }
| { tipo: 'erro'; mensagem: string };
function tratar(r: Resultado) {
switch (r.tipo) {
case 'sucesso':
return r.dados.length; // 'dados' existe apenas nesta variante
case 'erro':
return r.mensagem;
}
}
Uniões discriminadas são o padrão idiomático para modelar respostas de API que podem ter sucesso ou falha — algo que veremos ao consumir serviços no frontend.
Segurança contra null
Com a opção strictNullChecks ativada (padrão em projetos NestJS e Next.js),
null e undefined não podem ser atribuídos a qualquer tipo:
function buscar(id: number): Aluno | undefined {
return alunos.find(a => a.id === id);
}
const a = buscar(1);
// a.nome; // erro: possivelmente undefined
console.log(a?.nome); // ok
if (a) console.log(a.nome); // ok: estreitado
O operador ! (non-null assertion, como em a!.nome) desliga essa verificação
pontualmente. Use-o com muita parcimônia: ele transfere para você a
responsabilidade que o compilador estava assumindo.
Enums e a alternativa as const
enum Perfil {
ALUNO = 'ALUNO',
PROFESSOR = 'PROFESSOR',
ADMIN = 'ADMIN',
}
// alternativa sem gerar código em tempo de execução
const PERFIS = ['ALUNO', 'PROFESSOR', 'ADMIN'] as const;
type Perfil2 = typeof PERFIS[number]; // 'ALUNO' | 'PROFESSOR' | 'ADMIN'
Enums de string são comuns em código NestJS (perfis de acesso, estados de um
pedido). A forma com as const é mais leve e se integra melhor com JSON.
Parte 3 — Recursos que a stack exige
Generics
Generics permitem escrever código que funciona com vários tipos sem perder a informação de tipo.
function primeiro<T>(lista: T[]): T | undefined {
return lista[0];
}
const n = primeiro([1, 2, 3]); // number | undefined
const s = primeiro(['a', 'b']); // string | undefined
Aplicação típica: um envelope de resposta de API reutilizável.
interface RespostaApi<T> {
dados: T;
total: number;
pagina: number;
}
type RespostaAlunos = RespostaApi<Aluno[]>;
async function buscarPagina<T>(url: string): Promise<RespostaApi<T>> {
const r = await fetch(url);
return r.json() as Promise<RespostaApi<T>>;
}
Restrições limitam o que o parâmetro de tipo pode ser:
function porId<T extends { id: number }>(itens: T[], id: number): T | undefined {
return itens.find(item => item.id === id);
}
Utility types
TypeScript traz tipos utilitários que derivam um tipo de outro. Eles evitam duplicar declarações entre entidade, DTO de criação e DTO de atualização.
interface Aluno {
id: number;
nome: string;
email: string;
curso: string;
senha: string;
}
| Utility type | Resultado | Uso típico |
|---|---|---|
Partial<Aluno> | Todas as propriedades opcionais | Corpo de um PATCH |
Required<Aluno> | Todas obrigatórias | Normalizar um objeto de configuração |
Readonly<Aluno> | Todas somente leitura | Objetos imutáveis |
Pick<Aluno, 'id' | 'nome'> | Só as propriedades escolhidas | Projeção de listagem |
Omit<Aluno, 'senha'> | Todas menos as excluídas | Resposta pública da API |
Record<string, number> | Mapa chave → valor | Dicionários e índices |
ReturnType<typeof fn> | Tipo retornado pela função | Inferir tipos de helpers |
type AlunoPublico = Omit<Aluno, 'senha'>;
type CriarAlunoDto = Omit<Aluno, 'id'>;
type AtualizarAlunoDto = Partial<CriarAlunoDto>;
type ResumoAluno = Pick<Aluno, 'id' | 'nome'>;
Essa cadeia — entidade → DTO de criação → DTO de atualização — é exatamente a que o NestJS espera nos controllers.
Classes
class AlunoService {
private readonly alunos: Aluno[] = [];
constructor(private readonly repositorio: Repositorio) {} // parameter property
listar(): Aluno[] {
return this.alunos;
}
protected validar(aluno: Aluno): boolean {
return aluno.nome.trim().length > 0;
}
}
Modificadores: public (padrão), private, protected e readonly.
Declarar constructor(private readonly repositorio: Repositorio) cria o campo,
atribui o valor e define a visibilidade em uma linha só. Essa é a forma que o
NestJS usa para injeção de dependência: o framework lê os tipos dos
parâmetros do construtor e injeta as instâncias correspondentes — o mesmo papel
que a anotação @Inject do CDI cumpre em Jakarta EE.
Classes também podem implementar interfaces:
interface Notificador {
enviar(destino: string, mensagem: string): Promise<void>;
}
class EmailNotificador implements Notificador {
async enviar(destino: string, mensagem: string): Promise<void> {
// ...
}
}
Decorators
Um decorator é uma função que adiciona metadados ou comportamento a uma classe, método, propriedade ou parâmetro. É o mecanismo central do NestJS — o equivalente conceitual às anotações do Java.
@Controller('alunos') // classe é um controller na rota /alunos
export class AlunoController {
constructor(private readonly service: AlunoService) {}
@Get() // método responde a GET /alunos
listar(): Aluno[] {
return this.service.listar();
}
@Get(':id') // GET /alunos/42
buscar(@Param('id') id: string): Aluno {
return this.service.buscar(Number(id));
}
@Post() // POST /alunos
criar(@Body() dto: CriarAlunoDto): Aluno {
return this.service.criar(dto);
}
}
O que acontece entre o @ que você escreve e a instância que aparece no
construtor passa por três momentos:
Você usará decorators muito antes de precisar escrevê-los. Por ora, basta entender que:
- decorators são funções aplicadas com
@acima do elemento decorado; - eles registram metadados que o framework lê em tempo de inicialização;
- o NestJS exige
experimentalDecoratorseemitDecoratorMetadatanotsconfig.json, mais a bibliotecareflect-metadataimportada nomain.ts— tudo já configurado pelo CLI do NestJS.
Isso confunde bastante ao pesquisar na internet:
| Decorators legacy | Decorators padrão (TC39) | |
|---|---|---|
| Como ligar | "experimentalDecorators": true | Nada — funciona desde a versão 5.0 |
| Origem | Proposta antiga, nunca padronizada | Proposta TC39 estágio 3 |
| Decorators de parâmetro | Sim | Não existem |
| Metadados de tipo em runtime | Via emitDecoratorMetadata + reflect-metadata | Fora do escopo da proposta |
| Quem usa | NestJS, Angular, TypeORM | Código novo sem injeção por tipo |
O NestJS depende dos legacy justamente porque usa decorators de parâmetro
(@Param, @Body, @Inject) e porque a injeção de dependência precisa dos
metadados de tipo em tempo de execução — que, como vimos, o TypeScript apaga por
padrão. Ou seja: os dois modelos coexistem, e em projetos NestJS a opção
experimentalDecorators continua obrigatória.
Type guards personalizados
function ehAluno(valor: unknown): valor is Aluno {
return (
typeof valor === 'object' &&
valor !== null &&
'id' in valor &&
'nome' in valor
);
}
const dados: unknown = await resposta.json();
if (ehAluno(dados)) {
console.log(dados.nome); // tipo estreitado para Aluno
}
Este é o mal-entendido mais comum. Escrever const aluno = await r.json() as Aluno
não verifica nada em tempo de execução — é apenas uma promessa ao compilador.
Se a API devolver outro formato, o programa quebra adiante, longe da origem do
erro. Dados que vêm de fora (rede, formulário, arquivo) precisam de validação
em tempo de execução: class-validator no NestJS ou bibliotecas de esquema
como Zod no frontend.
Parte 4 — Configuração do projeto
O arquivo tsconfig.json controla o compilador. As opções que mais importam:
{
"compilerOptions": {
"target": "ES2023",
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": ["node"],
"skipLibCheck": true,
"paths": { "@/*": ["./src/*"] }
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
| Opção | Efeito |
|---|---|
strict | Liga o conjunto completo de verificações rigorosas. Mantenha ligado. Desde a versão 6.0 já é o padrão; deixar explícito documenta a intenção |
target | Versão de JavaScript gerada. O padrão passou a acompanhar a versão estável mais recente do ECMAScript |
module | Sistema de módulos da saída. O padrão passou a ser esnext; nodenext é o adequado para backend Node |
rootDir / outDir | Diretório de entrada e de saída. Desde a 6.0, rootDir não é mais inferido: se seu código está em src/, declare-o |
types | Pacotes de tipos globais carregados. Desde a 6.0 o padrão é [] — se você usa @types/node ou @types/jest, precisa listá-los |
experimentalDecorators | Habilita decorators legacy — necessário para NestJS |
emitDecoratorMetadata | Emite metadados de tipo — necessário para a injeção de dependência do NestJS |
paths | Aliases de importação (@/services/aluno em vez de ../../services/aluno) |
skipLibCheck | Não verifica os .d.ts das dependências — acelera muito a compilação |
Opções que deixaram de existir
Se você encontrar um tsconfig.json antigo, estas opções foram descontinuadas na
versão 6.0 e viraram erro na 7.0:
| Opção removida | Substituir por |
|---|---|
"target": "es5" | es2015 ou superior (o mínimo passou a ser ES2015) |
"downlevelIteration" | Nada — só fazia sentido com target: es5 |
"moduleResolution": "node" / "node10" / "classic" | nodenext (backend Node) ou bundler (frontend) |
"module": "amd", "umd", "systemjs", "none" | esnext ou preserve, com um bundler |
"baseUrl" | paths com caminhos relativos à raiz do projeto |
"esModuleInterop": false | Não é mais possível desligar — pode remover a linha |
nest new e create-next-app geram um tsconfig.json adequado. O objetivo aqui
é que você saiba ler o arquivo e entenda o efeito de cada opção quando
precisar ajustá-lo — e que reconheça, ao copiar configuração de um tutorial
antigo, o que já não vale mais.
Onde o TypeScript aparece nos dois lados da stack
- Backend (NestJS)
- Frontend (Next.js / React)
// alunos/dto/criar-aluno.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CriarAlunoDto {
@IsString()
@MinLength(3)
nome: string;
@IsEmail()
email: string;
@IsString()
curso: string;
}
Decorators de class-validator acrescentam validação em tempo de execução ao
que os tipos já descrevem em tempo de compilação. As duas camadas se completam.
type AlunoCardProps = {
aluno: Aluno;
destaque?: boolean;
onSelecionar: (id: number) => void;
};
export function AlunoCard({ aluno, destaque = false, onSelecionar }: AlunoCardProps) {
return (
<article onClick={() => onSelecionar(aluno.id)}>
<h3>{aluno.nome}</h3>
<p>{aluno.curso}</p>
</article>
);
}
As props de um componente são apenas um tipo de objeto. O compilador passa a avisar quando um componente é usado com propriedades faltando ou com o tipo errado.
O ganho real aparece quando o mesmo tipo descreve o recurso nos dois lados: o formato do aluno definido a partir do contrato da API vale para o controller do NestJS e para o componente React que o exibe. É esse fio que a próxima página, de API REST, começa a costurar.
Laboratórios Práticos
Os checkpoints da próxima seção verificam compreensão. Estes laboratórios verificam prática: são exercícios para digitar, rodar e ver quebrar. São seis, progressivos, e podem ser feitos em uma ou duas sessões.
Preparando o ambiente
- Playground (sem instalar nada)
- IDE local
Acesse o TypeScript Playground. Ele compila no próprio navegador e mostra três painéis úteis:
| Painel | Para que serve |
|---|---|
| .JS | O JavaScript gerado — é aqui que você vê os tipos sendo apagados |
| .D.TS | O arquivo de declarações que seria publicado |
| Errors | Todos os erros do arquivo, com o texto completo |
Ajustes recomendados antes de começar:
- Em TS Config, confirme que
strictestá marcado. - Em TS Config, escolha uma versão recente no seletor de versão.
- Use Share para gerar um link do seu código — é assim que você entrega o exercício ou pede ajuda.
Bom para: tipos, generics, utility types, narrowing. Não serve para:
decorators com metadados, import de pacotes npm, leitura de arquivos.
Requisitos: Node.js 20 ou superior e um editor com suporte a TypeScript (VS Code, WebStorm, Zed — todos trazem integração pronta).
mkdir lab-typescript && cd lab-typescript
npm init -y
npm install -D typescript tsx @types/node
npx tsc --init
mkdir src
Acrescente os scripts ao package.json:
{
"scripts": {
"check": "tsc --noEmit",
"dev": "tsx watch src/index.ts",
"build": "tsc"
}
}
| Comando | O que faz |
|---|---|
npm run check | Só verifica os tipos, não gera arquivos — é o que a CI roda |
npm run dev | Executa e reexecuta a cada alteração |
npm run build | Gera o JavaScript em dist/ |
1. Com moduleResolution: nodenext, importações relativas exigem a extensão
.js — mesmo que o arquivo de origem seja .ts:
import { resolver } from './di.js'; // correto
import { resolver } from './di'; // erro TS2307
Parece contraintuitivo, mas o caminho se refere ao arquivo gerado, e é o mesmo comportamento do Node com ESM.
2. tsx não processa decorators com metadados. Ele usa esbuild, que
transpila sem verificar tipos e não emite os metadados de
emitDecoratorMetadata. Para os laboratórios 1 a 5 isso é irrelevante e até
desejável (execução instantânea). No Laboratório 6, compile com tsc e rode
o JavaScript gerado com node.
Os laboratórios foram testados com Node.js 22 e TypeScript 7.0.2. Com
versões 5.x ou 6.x o comportamento é o mesmo, exceto pelos padrões de
configuração descritos na Parte 4 — nesse caso, declare "strict": true
explicitamente.
Laboratório 1 — Tipos, inferência e narrowing
Onde: Playground · Tempo: ~20 min · Objetivo: perceber o que o compilador infere sozinho e como as verificações estreitam um tipo.
-
Cole o código abaixo e observe os erros marcados.
type Status = 'rascunho' | 'publicado' | 'arquivado';interface Artigo {id: number;titulo: string;status: Status;publicadoEm?: Date;visualizacoes: number;}function descrever(artigo: Artigo): string {return `${artigo.titulo} (${artigo.status}) — publicado em ${artigo.publicadoEm.toISOString()}`;} -
Corrija o erro de
publicadoEmde três formas diferentes: com?., com umifde estreitamento e com??fornecendo um texto alternativo. Compare o JavaScript gerado no painel .JS em cada caso. -
Escreva
function resumo(a: Artigo)que retorne um texto diferente para cada valor destatus, usandoswitch. Depois acrescente'em-revisao'ao tipoStatussem alterar a função e observe o erro. -
Passe o mouse sobre cada variável e anote qual tipo o compilador inferiu — sem você ter escrito nenhuma anotação.
Pronto quando: o painel Errors estiver vazio, a função resumo cobrir os
quatro valores de Status e você souber explicar por que o painel .JS não
contém nenhuma das interfaces que você escreveu.
Laboratório 2 — DTOs com utility types
Onde: Playground · Tempo: ~25 min · Objetivo: derivar contratos de API de um único tipo-fonte, sem duplicação.
Partindo de:
interface Usuario {
id: number;
nome: string;
email: string;
senhaHash: string;
perfil: 'aluno' | 'professor' | 'admin';
criadoEm: Date;
ultimoAcesso: Date | null;
}
Declare, usando apenas utility types (sem repetir campos):
UsuarioPublico— semsenhaHash.CriarUsuarioDto— o que o cliente envia noPOST: semid, semsenhaHash, semcriadoEm, semultimoAcesso, mas com um camposenha: string.AtualizarUsuarioDto— todos os campos de criação, opcionais.ResumoUsuario— apenasid,nomeeperfil.IndicePorPerfil— um mapa do perfil para a lista de resumos.UsuarioImutavel—UsuarioPublicocom todos os campos somente leitura. Tente atribuir a uma propriedade e confirme o erro.
Dica: Omit, Pick, Partial, Readonly, Record e a interseção &
resolvem todos os seis.
Pronto quando: cada tipo compilar e você conseguir criar um objeto de exemplo para cada um sem que o compilador reclame — e sem ter escrito o nome de nenhum campo duas vezes.
Laboratório 3 — Um projeto do zero e o efeito do strict
Onde: IDE local · Tempo: ~30 min · Objetivo: entender na prática o que o modo estrito detecta.
-
No projeto criado acima, edite o
tsconfig.jsone coloque temporariamente"strict": false. -
Crie
src/carrinho.ts:const itens = [{ nome: 'Caderno', preco: 24.9, qtd: 2 },{ nome: 'Caneta', preco: 4.5, qtd: 10 },];function total(carrinho) {let soma = 0;for (const item of carrinho) {soma += item.preco * item.qtd;}return soma;}function buscar(nome) {return itens.find(i => i.nome === nome);}console.log(total(itens));console.log(buscar('Régua').preco); -
Rode
npm run check. Nenhum erro aparece. -
Rode
npx tsx src/carrinho.ts. O programa imprime o total e depois quebra:94.8TypeError: Cannot read properties of undefined (reading 'preco') -
Volte
"strict": truee rodenpm run checkde novo. Agora surgem três erros:src/carrinho.ts(6,16): error TS7006: Parameter 'carrinho' implicitly has an 'any' type.src/carrinho.ts(14,17): error TS7006: Parameter 'nome' implicitly has an 'any' type.src/carrinho.ts(19,13): error TS2532: Object is possibly 'undefined'. -
Corrija todos: tipe o parâmetro
carrinho, tipenomee trate o caso em quebuscarnão encontra nada.
Pronto quando: npm run check não reportar nada e npx tsx src/carrinho.ts
imprimir o total sem quebrar, mesmo procurando por um item inexistente.
Reflita: o TypeError do passo 4 é literalmente o mesmo problema que o
TS2532 do passo 5 aponta — só que um aparece em produção e o outro, no editor,
antes do commit. Escreva uma frase explicando o valor disso em um backend que
atende dois clientes.
Laboratório 4 — Generics em uso real
Onde: IDE local · Tempo: ~35 min · Objetivo: escrever funções genéricas que preservam o tipo de entrada.
-
Em
src/colecoes.ts, implemente e exporte:// agrupa itens por uma chave calculadaexport function agruparPor<T>(itens: T[],chave: (item: T) => string,): Record<string, T[]> {// implemente}// indexa itens por uma propriedade que seja number ou stringexport function indexarPor<T, K extends keyof T>(itens: T[],propriedade: K,): Map<T[K], T> {// implemente}// ordena sem mutar o array originalexport function ordenarPor<T>(itens: T[],chave: (item: T) => number | string,direcao: 'asc' | 'desc' = 'asc',): T[] {// implemente} -
Em
src/index.ts, use as três com uma lista de alunos ({ id, nome, curso, nota }) e confirme, passando o mouse sobre os resultados, que o tipo do elemento não virouanyem nenhum momento. -
Crie um envelope de resposta e um cliente HTTP genéricos:
export interface Pagina<T> {dados: T[];pagina: number;total: number;}export async function buscarPagina<T>(url: string): Promise<Pagina<T>> {// implemente com fetch} -
Chame
buscarPaginacom um tipo concreto e verifique que o autocompletar do editor conhece os campos dedados[0].
Pronto quando: npm run check passar, as três funções tiverem testes manuais
no index.ts e você conseguir explicar o que a restrição K extends keyof T
impede que aconteça.
Laboratório 5 — Dados externos não têm tipo
Onde: IDE local · Tempo: ~30 min · Objetivo: ver por que uma anotação de tipo não protege contra uma API que mudou.
-
Crie
src/externo.tse busque dados de uma API pública — por exemplo a API de localidades do IBGE:interface Estado {id: number;sigla: string;nome: string;}async function main() {const r = await fetch('https://servicodados.ibge.gov.br/api/v1/localidades/estados');const estados = (await r.json()) as Estado[]; // ← promessa vazia ao compiladorconsole.log(estados.map(e => e.sigla).join(', '));}main(); -
Agora sabote o contrato: troque o nome do campo na interface de
siglaparaufe ajuste oconsole.log. O compilador fica satisfeito. Rode. O que é impresso? -
Reescreva sem
as, usando um type guard:function ehEstado(v: unknown): v is Estado {// implemente as verificações}function ehListaDeEstados(v: unknown): v is Estado[] {return Array.isArray(v) && v.every(ehEstado);}Se a validação falhar, lance um erro com uma mensagem clara.
-
Repita a sabotagem do passo 2. Desta vez o programa deve falhar na fronteira, com uma mensagem que diz exatamente o que está errado.
-
(Opcional) Instale uma biblioteca de esquema (
npm install zod) e substitua os type guards manuais por um esquema declarativo. Compare o volume de código e a qualidade da mensagem de erro.
Pronto quando: com o contrato quebrado, o programa parar imediatamente na
validação em vez de imprimir undefined — e você conseguir explicar por que o
as do passo 1 não ajudou em nada.
Laboratório 6 — Decorators e injeção de dependência
Onde: IDE local · Tempo: ~40 min · Objetivo: desmistificar o @ do
NestJS construindo uma versão mínima do mecanismo.
Este laboratório é opcional e mais difícil. Ele não é pré-requisito para o Módulo 2 — o NestJS esconde tudo isso. Faça se quiser entender o que acontece por baixo.
-
Prepare o projeto:
npm install reflect-metadataNo
tsconfig.json, confirmeexperimentalDecoratorseemitDecoratorMetadatacomotrue. -
Crie
src/di.ts:import 'reflect-metadata';const container = new Map<Function, unknown>();// decorator de classe: registra a classe como injetávelexport function Injetavel(): ClassDecorator {return (alvo) => {Reflect.defineMetadata('injetavel', true, alvo);};}// resolve uma classe lendo os tipos do construtorexport function resolver<T>(Classe: new (...args: any[]) => T): T {if (container.has(Classe)) return container.get(Classe) as T;const tipos: any[] = Reflect.getMetadata('design:paramtypes', Classe) ?? [];const dependencias = tipos.map(tipo => resolver(tipo));const instancia = new Classe(...dependencias);container.set(Classe, instancia);return instancia;} -
Crie três classes encadeadas — um repositório, um serviço que depende dele e um controller que depende do serviço — todas com
@Injetavel()e usando parameter properties no construtor. -
Chame
resolver(AlunoController)e confirme que a cadeia inteira foi instanciada sozinha. Compile e rode:npm run build && node dist/index.js -
Experimento decisivo: desligue
emitDecoratorMetadatanotsconfig.json, apaguedist/, recompile e rode de novo. O programa passa a falhar com:TypeError: Cannot read properties of undefined (reading 'listar')Explique por quê — e note que o
tscnão reclamou de nada.
Pronto quando: a resolução automática funcionar, e você conseguir apontar,
no JavaScript gerado em dist/, a linha em que os tipos do construtor foram
gravados como metadados.
Reflita: é exatamente isso que o NestJS faz ao ler
constructor(private readonly service: AlunoService). Compare com o que o CDI
faz em Jakarta EE, onde os tipos existem no bytecode e não precisam ser emitidos.
Erros comuns
| Erro | Por que acontece | Como evitar |
|---|---|---|
Usar any para "resolver" um erro do compilador | Silencia o diagnóstico sem corrigir a causa | Use unknown + narrowing, ou corrija o tipo |
Confiar em as Tipo para dados de API | Cast não valida nada em tempo de execução | Valide com class-validator/Zod |
| Confundir ` | com??` | |
Mutar estado com push/sort no React | A interface não re-renderiza | Use spread, map, toSorted(), with() |
Esquecer await | Recebe uma Promise em vez do valor | Ative a regra de lint no-floating-promises |
| Achar que tipos existem em produção | Tipos são apagados na transpilação | Toda verificação de dados externos é código, não tipo |
Copiar tsconfig.json de tutorial antigo | Opções como baseUrl, target: es5 e moduleResolution: node viraram erro | Gere com tsc --init ou com o CLI do framework |
| Estranhar erros ao colar código de exemplo | strict passou a ser o padrão | Adicione as anotações — o erro está apontando um risco real |
Exercícios (Checkpoints)
Estas questões verificam compreensão e podem ser respondidas por escrito. A parte prática está nos Laboratórios Práticos.
-
Explique, com suas palavras, por que um erro de tipo em TypeScript nunca chega ao ambiente de produção — e por que, ainda assim, é possível que uma aplicação TypeScript quebre em produção por causa de um dado com formato inesperado.
-
Compare duas formas de tornar um campo opcional: declarar
email?: stringna interface original e derivarPartial<Usuario>de uma interface com o campo obrigatório. Em que situação cada uma é a escolha correta? -
Justifique: por que
distanciaOrigem(posicao)compila quandoposicaotem uma propriedade a mais, masdistanciaOrigem({ x: 3, y: 4, cor: 'azul' })não? O que a regra do excess property check está tentando proteger? -
Reescreva o trecho abaixo eliminando o
anye tratando corretamente a ausência de valor:function precoTotal(carrinho: any) {let total = 0;for (const item of carrinho.itens) {total += item.preco * item.qtd;}return total;} -
Modele com uma união discriminada o resultado de uma chamada de API que pode retornar sucesso (com uma lista de alunos), erro de validação (com a lista de campos inválidos) ou erro de servidor (com uma mensagem). Escreva uma função que trate as três variantes e explique por que o compilador garante que nenhuma foi esquecida.
-
Compare a injeção de dependência via parameter properties do TypeScript/NestJS com a injeção via
@Injectdo CDI em Jakarta EE: o que é análogo e o que muda por os tipos não existirem em tempo de execução no TypeScript? -
Discuta as consequências práticas de
strictter passado a ser o padrão a partir da versão 6.0: o que muda ao copiar código de tutoriais antigos, e por que uma equipe pode acabar rodando duas versões do compilador (uma na CI, outra no editor) durante a transição para a 7.0?
Referências
Principais
- TypeScript Handbook — documentação oficial da linguagem
- TypeScript Playground — ambiente usado nos laboratórios 1 e 2
- TypeScript for Java/C# Programmers — guia de transição para quem vem de linguagens nominais
- TSConfig Reference — todas as opções do compilador
- JavaScript — MDN Web Docs — referência da linguagem base
Aprofundamento
- Utility Types — catálogo completo
- Announcing TypeScript 7.0 — o port nativo em Go, novos padrões e opções removidas (julho de 2026)
- Announcing TypeScript 6.0 — mudanças de padrão e deprecations que a 7.0 transformou em erro (março de 2026)
- Decorators — TypeScript Handbook — referência dos decorators legacy usados pelo NestJS
- NestJS — First steps — como os recursos vistos aqui aparecem no backend
- Next.js — TypeScript — configuração no frontend
- Type Challenges — exercícios progressivos sobre o sistema de tipos