Pular para o conteúdo principal

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.

Também serve como referência

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 null e undefined.
  • 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.json que 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.

Pipeline de transpilação: um arquivo .ts com anotações de tipo passa pelo compilador tsc, que verifica os tipos e depois os apaga, produzindo um .js sem nenhum tipo, que é o código efetivamente executado por Node.js ou pelo navegador

Para quem vem de Java ou C#, a mudança de perspectiva é a seguinte:

AspectoJavaTypeScript
Verificação de tiposEm tempo de compilação, obrigatóriaEm tempo de compilação, opcional e gradual
Tipos em tempo de execuçãoExistem (reflection, instanceof)Não existem — são apagados na transpilação
Sistema de tiposNominal (compatibilidade por nome da classe)Estrutural (compatibilidade por formato do objeto)
Unidade de compilaçãoClasseMódulo (arquivo)
HerançaClasses e interfacesClasses, 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ãoQuandoO que significa
5.xaté 2025Base do que a maior parte dos tutoriais e projetos ainda usa
6.0març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.0julho de 2026Reescrita 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.

Duas pegadinhas da versão 7.0
  1. strict agora é true por 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.
  2. 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 tsc da CI e mantêm a 6.0 no editor.
Exemplos executáveis

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 nadaTypeScript 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.

observação

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 seta

Funçõ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 }
Cópia rasa

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étodoRetornaMuta o array original
mapNovo array transformadoNão
filterNovo array filtradoNão
findElemento ou undefinedNão
reduceValor acumuladoNão
some / everybooleanNão
sort / reverseO próprio arraySim
push / spliceDependeSim
aviso

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 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);

Regras importantes:

  • Toda função marcada com async retorna uma Promise, mesmo que retorne um valor simples.
  • await só pode ser usado dentro de funções async (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';
CommonJS × ESM

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

TipoUsoObservação
string, number, booleanPrimitivos básicosnumber cobre inteiros e decimais (IEEE 754)
bigintInteiros arbitrariamente grandesLiteral com sufixo n: 10n
null / undefinedAusência de valorCom strictNullChecks, não são atribuíveis a outros tipos
anyDesliga a verificaçãoEvitar — anula o benefício do TypeScript
unknownValor de tipo desconhecidoAlternativa segura ao any: exige verificação antes do uso
neverNunca ocorreRetorno de função que sempre lança erro; usado em verificações de exaustividade
voidFunção sem retorno útilDiferente 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 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;
}

Quando usar cada um:

SituaçãoRecomendação
Formato de um objeto ou contrato de classeinterface
União, interseção, tupla, tipo de funçãotype
Precisa ser estendido por bibliotecas externasinterface
Tipo derivado com utility typestype

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.

Comparação entre o sistema nominal do Java, em que só é compatível a classe que declara implements, e o sistema estrutural do TypeScript, em que qualquer objeto com as propriedades exigidas é compatível, inclusive com propriedades a mais
Uma exceção: excess property check

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.

Estreitamento progressivo de um valor do tipo number, string ou null: cada guarda de tipo elimina variantes até restar uma só em cada ramo do código
// 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
dica

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 typeResultadoUso típico
Partial<Aluno>Todas as propriedades opcionaisCorpo de um PATCH
Required<Aluno>Todas obrigatóriasNormalizar um objeto de configuração
Readonly<Aluno>Todas somente leituraObjetos imutáveis
Pick<Aluno, 'id' | 'nome'>Só as propriedades escolhidasProjeção de listagem
Omit<Aluno, 'senha'>Todas menos as excluídasResposta pública da API
Record<string, number>Mapa chave → valorDicionários e índices
ReturnType<typeof fn>Tipo retornado pela funçãoInferir 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.

Parameter properties

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:

Do código com decorators aos metadados emitidos pelo compilador e ao contêiner do NestJS, que lê esses metadados no boot, resolve o grafo de dependências e injeta as instâncias no construtor

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 experimentalDecorators e emitDecoratorMetadata no tsconfig.json, mais a biblioteca reflect-metadata importada no main.ts — tudo já configurado pelo CLI do NestJS.
Existem dois "decorators" em TypeScript

Isso confunde bastante ao pesquisar na internet:

Decorators legacyDecorators padrão (TC39)
Como ligar"experimentalDecorators": trueNada — funciona desde a versão 5.0
OrigemProposta antiga, nunca padronizadaProposta TC39 estágio 3
Decorators de parâmetroSimNão existem
Metadados de tipo em runtimeVia emitDecoratorMetadata + reflect-metadataFora do escopo da proposta
Quem usaNestJS, Angular, TypeORMCó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
}
Tipos não validam dados externos

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çãoEfeito
strictLiga 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
targetVersão de JavaScript gerada. O padrão passou a acompanhar a versão estável mais recente do ECMAScript
moduleSistema de módulos da saída. O padrão passou a ser esnext; nodenext é o adequado para backend Node
rootDir / outDirDiretó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
typesPacotes de tipos globais carregados. Desde a 6.0 o padrão é [] — se você usa @types/node ou @types/jest, precisa listá-los
experimentalDecoratorsHabilita decorators legacy — necessário para NestJS
emitDecoratorMetadataEmite metadados de tipo — necessário para a injeção de dependência do NestJS
pathsAliases de importação (@/services/aluno em vez de ../../services/aluno)
skipLibCheckNã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 removidaSubstituir 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": falseNão é mais possível desligar — pode remover a linha
Você raramente escreve isso do zero

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

// 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.

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

Acesse o TypeScript Playground. Ele compila no próprio navegador e mostra três painéis úteis:

PainelPara que serve
.JSO JavaScript gerado — é aqui que você vê os tipos sendo apagados
.D.TSO arquivo de declarações que seria publicado
ErrorsTodos os erros do arquivo, com o texto completo

Ajustes recomendados antes de começar:

  1. Em TS Config, confirme que strict está marcado.
  2. Em TS Config, escolha uma versão recente no seletor de versão.
  3. 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.


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.

  1. 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()}`;
    }
  2. Corrija o erro de publicadoEm de três formas diferentes: com ?., com um if de estreitamento e com ?? fornecendo um texto alternativo. Compare o JavaScript gerado no painel .JS em cada caso.

  3. Escreva function resumo(a: Artigo) que retorne um texto diferente para cada valor de status, usando switch. Depois acrescente 'em-revisao' ao tipo Status sem alterar a função e observe o erro.

  4. 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):

  1. UsuarioPublico — sem senhaHash.
  2. CriarUsuarioDto — o que o cliente envia no POST: sem id, sem senhaHash, sem criadoEm, sem ultimoAcesso, mas com um campo senha: string.
  3. AtualizarUsuarioDto — todos os campos de criação, opcionais.
  4. ResumoUsuario — apenas id, nome e perfil.
  5. IndicePorPerfil — um mapa do perfil para a lista de resumos.
  6. UsuarioImutavelUsuarioPublico com 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.

  1. No projeto criado acima, edite o tsconfig.json e coloque temporariamente "strict": false.

  2. 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);
  3. Rode npm run check. Nenhum erro aparece.

  4. Rode npx tsx src/carrinho.ts. O programa imprime o total e depois quebra:

    94.8
    TypeError: Cannot read properties of undefined (reading 'preco')
  5. Volte "strict": true e rode npm run check de 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'.
  6. Corrija todos: tipe o parâmetro carrinho, tipe nome e trate o caso em que buscar nã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.

  1. Em src/colecoes.ts, implemente e exporte:

    // agrupa itens por uma chave calculada
    export function agruparPor<T>(
    itens: T[],
    chave: (item: T) => string,
    ): Record<string, T[]> {
    // implemente
    }

    // indexa itens por uma propriedade que seja number ou string
    export function indexarPor<T, K extends keyof T>(
    itens: T[],
    propriedade: K,
    ): Map<T[K], T> {
    // implemente
    }

    // ordena sem mutar o array original
    export function ordenarPor<T>(
    itens: T[],
    chave: (item: T) => number | string,
    direcao: 'asc' | 'desc' = 'asc',
    ): T[] {
    // implemente
    }
  2. 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 virou any em nenhum momento.

  3. 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
    }
  4. Chame buscarPagina com um tipo concreto e verifique que o autocompletar do editor conhece os campos de dados[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.

  1. Crie src/externo.ts e 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 compilador

    console.log(estados.map(e => e.sigla).join(', '));
    }

    main();
  2. Agora sabote o contrato: troque o nome do campo na interface de sigla para uf e ajuste o console.log. O compilador fica satisfeito. Rode. O que é impresso?

  3. 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.

  4. Repita a sabotagem do passo 2. Desta vez o programa deve falhar na fronteira, com uma mensagem que diz exatamente o que está errado.

  5. (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.

observação

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.

  1. Prepare o projeto:

    npm install reflect-metadata

    No tsconfig.json, confirme experimentalDecorators e emitDecoratorMetadata como true.

  2. Crie src/di.ts:

    import 'reflect-metadata';

    const container = new Map<Function, unknown>();

    // decorator de classe: registra a classe como injetável
    export function Injetavel(): ClassDecorator {
    return (alvo) => {
    Reflect.defineMetadata('injetavel', true, alvo);
    };
    }

    // resolve uma classe lendo os tipos do construtor
    export 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;
    }
  3. 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.

  4. Chame resolver(AlunoController) e confirme que a cadeia inteira foi instanciada sozinha. Compile e rode:

    npm run build && node dist/index.js
  5. Experimento decisivo: desligue emitDecoratorMetadata no tsconfig.json, apague dist/, 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 tsc nã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

ErroPor que aconteceComo evitar
Usar any para "resolver" um erro do compiladorSilencia o diagnóstico sem corrigir a causaUse unknown + narrowing, ou corrija o tipo
Confiar em as Tipo para dados de APICast não valida nada em tempo de execuçãoValide com class-validator/Zod
Confundir `com??`
Mutar estado com push/sort no ReactA interface não re-renderizaUse spread, map, toSorted(), with()
Esquecer awaitRecebe uma Promise em vez do valorAtive a regra de lint no-floating-promises
Achar que tipos existem em produçãoTipos são apagados na transpilaçãoToda verificação de dados externos é código, não tipo
Copiar tsconfig.json de tutorial antigoOpções como baseUrl, target: es5 e moduleResolution: node viraram erroGere com tsc --init ou com o CLI do framework
Estranhar erros ao colar código de exemplostrict passou a ser o padrãoAdicione 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.

  1. 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.

  2. Compare duas formas de tornar um campo opcional: declarar email?: string na interface original e derivar Partial<Usuario> de uma interface com o campo obrigatório. Em que situação cada uma é a escolha correta?

  3. Justifique: por que distanciaOrigem(posicao) compila quando posicao tem uma propriedade a mais, mas distanciaOrigem({ x: 3, y: 4, cor: 'azul' }) não? O que a regra do excess property check está tentando proteger?

  4. Reescreva o trecho abaixo eliminando o any e 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;
    }
  5. 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.

  6. Compare a injeção de dependência via parameter properties do TypeScript/NestJS com a injeção via @Inject do 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?

  7. Discuta as consequências práticas de strict ter 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

Aprofundamento