JavaScript é uma linguagem dinamicamente tipada. Isso significa que uma variável pode ser um número agora, uma string depois, e undefined mais tarde — e o interpretador não reclama. Isso oferece flexibilidade, mas também é a fonte de uma classe inteira de bugs que só aparecem em produção, à meia-noite, no pior momento possível.
O TypeScript resolve isso adicionando um sistema de tipos ao JavaScript. Não é uma linguagem nova — é JavaScript com superpoderes que some em tempo de execução. Todo código TypeScript vira JavaScript. Todo código JavaScript válido é TypeScript válido.
Por que TypeScript?
// ── JavaScript ─────────────────────────────────────
function calcularDesconto(preco, percentual) {
return preco - (preco * percentual / 100);
}
calcularDesconto(100, 10); // 90 ✅
calcularDesconto("100", 10); // 90 😱 — funciona, e esse é o problema
calcularDesconto("100,50", 10); // NaN 😱 a vírgula decimal quebra tudo
calcularDesconto(100, "dez"); // NaN 😱
calcularDesconto(); // NaN 😱 sem argumento, sem aviso
// A segunda linha merece atenção: - e * CONVERTEM a string em número, então
// o resultado sai certo por acidente e o defeito fica escondido até a
// entrada vir num formato diferente. Quem concatena é o +, e aí sim:
function somarAoTotal(total, preco) { return total + preco; }
somarAoTotal(100, "10"); // "10010" 😱 aí sim, concatenação
// ── TypeScript ─────────────────────────────────────
function calcularDesconto(preco: number, percentual: number): number {
return preco - (preco * percentual / 100);
}
calcularDesconto(100, 10); // 90 ✅
calcularDesconto("100", 10); // ❌ Erro em tempo de compilação
calcularDesconto(100, "dez"); // ❌ Erro em tempo de compilação
calcularDesconto(); // ❌ Erro em tempo de compilação
O erro aparece antes de rodar — no editor, enquanto você digita.
Instalando e configurando
# Instalar globalmente (para o compilador tsc)
npm install -g typescript
# Instalar no projeto
npm install -D typescript
# Inicializar configuração
npx tsc --init
// tsconfig.json — configuração essencial para Node.js
{
"compilerOptions": {
"target": "ES2022", // versão do JavaScript gerado
"module": "commonjs", // sistema de módulos (Node)
"lib": ["ES2022"], // bibliotecas disponíveis
"outDir": "./dist", // onde o JS compilado vai
"rootDir": "./src", // onde está o TypeScript
"strict": true, // ativa todas as verificações rígidas
"esModuleInterop": true, // compatibilidade com CommonJS
"skipLibCheck": true, // ignora erros em node_modules
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, // permite import de JSON
"declaration": true, // gera arquivos .d.ts
"sourceMap": true, // mapas para debugging
"noUnusedLocals": true, // erro para variáveis não usadas
"noUnusedParameters": true, // erro para parâmetros não usados
"noImplicitReturns": true, // toda função deve retornar
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
// package.json — scripts para TypeScript
{
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"dev": "ts-node-dev src/index.ts",
"start": "node dist/index.js"
}
}
npm install -D ts-node-dev @types/node
# ts-node-dev → equivalente ao nodemon para TypeScript
Tipos primitivos e básicos
// ── Primitivos ──────────────────────────────────────
let nome: string = "Ana";
let idade: number = 28;
let ativo: boolean = true;
let nada: null = null;
let indefinido: undefined = undefined;
// Inferência de tipos — TypeScript deduz o tipo automaticamente
let cidade = "São Paulo"; // TypeScript infere: string
let pontos = 100; // TypeScript infere: number
cidade = 42; // ❌ Erro: não pode ser number
// ── Arrays ─────────────────────────────────────────
let numeros: number[] = [1, 2, 3];
let nomes: string[] = ["Ana", "Bruno"];
let misturado: (string | number)[] = ["Ana", 42];
// Alternativa com genérico
let ids: Array<number> = [1, 2, 3];
// ── Tuplas — array com tipos fixos por posição ─────
let par: [string, number] = ["Ana", 28];
let coordenada: [number, number, number] = [10.5, -23.4, 0];
// ── any — o tipo que desabilita TypeScript ─────────
let qualquerCoisa: any = "texto";
qualquerCoisa = 42; // ok — mas perde a proteção do TS
qualquerCoisa = { a: 1 }; // ok — mas evite ao máximo
// ── unknown — alternativa segura ao any ────────────
let entrada: unknown = obterEntradaExterna();
// entrada.toUpperCase(); // ❌ Erro — não sabe o tipo ainda
if (typeof entrada === "string") {
entrada.toUpperCase(); // ✅ verificou o tipo primeiro
}
// ── never — código que nunca retorna ───────────────
function lancarErro(msg: string): never {
throw new Error(msg); // nunca retorna normalmente
}
Interfaces — definindo contratos
// Interface define a "forma" de um objeto
interface Usuario {
id: number;
nome: string;
email: string;
idade?: number; // ? = opcional
readonly criadoEm: Date; // readonly = imutável após criação
}
// Usando
const usuario: Usuario = {
id: 1,
nome: "Ana Paula",
email: "ana@email.com",
criadoEm: new Date(),
};
usuario.nome = "Ana"; // ✅ pode modificar
// usuario.criadoEm = new Date(); // ❌ readonly!
// Interface de função
interface Comparador {
(a: number, b: number): number;
}
const ordenarCrescente: Comparador = (a, b) => a - b;
// Interface com métodos
interface Repositorio<T> {
buscarPorId(id: number): Promise<T | null>;
listar(): Promise<T[]>;
salvar(item: T): Promise<T>;
remover(id: number): Promise<void>;
}
// Extendendo interfaces
interface UsuarioAdmin extends Usuario {
permissoes: string[];
nivel: "super" | "comum";
}
// Implementando interface em classe
class UsuarioRepositorio implements Repositorio<Usuario> {
async buscarPorId(id: number): Promise<Usuario | null> {
// implementação...
return null;
}
async listar(): Promise<Usuario[]> {
return [];
}
async salvar(usuario: Usuario): Promise<Usuario> {
return usuario;
}
async remover(id: number): Promise<void> {
// implementação...
}
}
Type Aliases — nomes para tipos
// Type alias — nome para qualquer tipo
type ID = number | string;
type Email = string;
type Status = "pendente" | "ativo" | "inativo"; // union literal
type Coordenada = {
lat: number;
lon: number;
};
type Callback<T> = (erro: Error | null, resultado: T | null) => void;
// Usando
let id: ID = 42;
id = "abc-123"; // também válido
let status: Status = "ativo";
// status = "deletado"; // ❌ não está no union
// ── Interface vs Type — quando usar cada um ─────────
// Interface: objetos e classes → prefira para APIs públicas
// Type: unions, intersections, primitivos → mais versátil
// Intersection types — combina tipos
type UsuarioComToken = Usuario & {
token: string;
expiraEm: Date;
};
Generics — tipos reutilizáveis
Generics permitem escrever código que funciona com qualquer tipo mantendo segurança:
// Função genérica
function primeiroItem<T>(array: T[]): T | undefined {
return array[0];
}
const num = primeiroItem([1, 2, 3]); // TypeScript infere: number
const str = primeiroItem(["a", "b"]); // TypeScript infere: string
const vazio = primeiroItem([]); // TypeScript infere: undefined
// Resposta de API genérica
interface RespostaAPI<T> {
dados: T;
sucesso: boolean;
mensagem: string;
timestamp: string;
}
type RespostaUsuario = RespostaAPI<Usuario>;
type RespostaLista = RespostaAPI<Usuario[]>;
// Função com múltiplos genéricos
function mapear<Entrada, Saida>(
array: Entrada[],
transformar: (item: Entrada) => Saida
): Saida[] {
return array.map(transformar);
}
const nomes = mapear(
[{ id: 1, nome: "Ana" }, { id: 2, nome: "Bruno" }],
(u) => u.nome
);
// nomes: string[] — TypeScript inferiu!
// Generic com restrição (extends)
function buscarPropriedade<T, K extends keyof T>(obj: T, chave: K): T[K] {
return obj[chave];
}
const usuario = { nome: "Ana", idade: 28, ativo: true };
const nome = buscarPropriedade(usuario, "nome"); // string
const idade = buscarPropriedade(usuario, "idade"); // number
// buscarPropriedade(usuario, "inexistente"); // ❌ Erro
Utility Types — tipos prontos do TypeScript
interface Usuario {
id: number;
nome: string;
email: string;
senha: string;
ativo: boolean;
}
// Partial<T> — todos os campos opcionais
type AtualizacaoUsuario = Partial<Usuario>;
// { id?: number, nome?: string, email?: string, ... }
// Required<T> — todos os campos obrigatórios
type UsuarioCompleto = Required<AtualizacaoUsuario>;
// Readonly<T> — todos os campos imutáveis
type UsuarioImutavel = Readonly<Usuario>;
// Pick<T, K> — seleciona apenas alguns campos
type UsuarioPublico = Pick<Usuario, "id" | "nome" | "email">;
// { id: number, nome: string, email: string }
// Omit<T, K> — remove campos
type UsuarioSemSenha = Omit<Usuario, "senha">;
// { id: number, nome: string, email: string, ativo: boolean }
// Record<K, V> — objeto com chaves e valores tipados
type MapaDeErros = Record<string, string[]>;
// { email: ["inválido"], nome: ["muito curto"] }
type StatusPorId = Record<number, "ativo" | "inativo">;
// Exclude<T, U> — remove tipos de um union
type SemNull = Exclude<string | number | null | undefined, null | undefined>;
// string | number
// NonNullable<T> — remove null e undefined
type Obrigatorio = NonNullable<string | null | undefined>;
// string
// ReturnType<T> — tipo de retorno de uma função
function criarUsuario() {
return { id: 1, nome: "Ana" };
}
type TipoRetorno = ReturnType<typeof criarUsuario>;
// { id: number, nome: string }
// Parameters<T> — tipos dos parâmetros de uma função
type Params = Parameters<typeof calcularDesconto>;
// [preco: number, percentual: number]
Enums — conjuntos de constantes nomeadas
// Enum numérico (padrão)
enum Status {
Pendente, // 0
Ativo, // 1
Inativo, // 2
Bloqueado, // 3
}
const status = Status.Ativo; // 1
console.log(Status[1]); // "Ativo" — mapeamento reverso
// Enum de string (mais legível, recomendado)
enum Prioridade {
Baixa = "baixa",
Media = "media",
Alta = "alta",
Critica = "critica",
}
// Const enum — o valor é embutido direto na compilação, sem gerar objeto.
// Em compensação, ele não funciona com isolatedModules, que é o padrão de
// quem compila com esbuild, swc ou Vite — ou seja, quase todo projeto novo.
const enum DirecaoHTTP {
Get = "GET",
Post = "POST",
Put = "PUT",
Delete = "DELETE",
}
// Union literal — alternativa moderna ao enum
type PrioridadeType = "baixa" | "media" | "alta" | "critica";
// Mais simples, sem overhead, amplamente preferido em código moderno
Classes com TypeScript
class Tarefa {
// Propriedades com modificadores de acesso
public readonly id: number;
public titulo: string;
public descricao: string;
private _concluida: boolean = false; // _ = convenção para privado
protected criadoEm: Date;
// Construtor com parâmetros tipados
constructor(
id: number,
titulo: string,
descricao: string = "",
) {
this.id = id;
this.titulo = titulo;
this.descricao = descricao;
this.criadoEm = new Date();
}
// Getter — propriedade calculada
get concluida(): boolean {
return this._concluida;
}
// Setter — com validação
set concluida(valor: boolean) {
if (this._concluida && !valor) {
throw new Error("Não é possível reabrir uma tarefa concluída.");
}
this._concluida = valor;
}
// Método com tipo de retorno explícito
concluir(): void {
this._concluida = true;
}
// Método estático
static criar(titulo: string): Tarefa {
return new Tarefa(Date.now(), titulo);
}
// Convertendo para JSON
toJSON(): object {
return {
id: this.id,
titulo: this.titulo,
descricao: this.descricao,
concluida: this._concluida,
criadoEm: this.criadoEm,
};
}
}
// Herança
class TarefaUrgente extends Tarefa {
public prazo: Date;
constructor(id: number, titulo: string, prazo: Date) {
super(id, titulo); // chama construtor da classe pai
this.prazo = prazo;
}
get atrasada(): boolean {
return !this.concluida && new Date() > this.prazo;
}
// Override do método pai
override toJSON(): object {
return {
...super.toJSON(),
prazo: this.prazo,
atrasada: this.atrasada,
};
}
}
// Shorthand de construtor — elimina boilerplate
class Produto {
constructor(
public readonly id: number,
public nome: string,
private preco: number,
protected estoque: number = 0,
) {}
aumentarEstoque(quantidade: number): void {
this.estoque += quantidade;
}
}
TypeScript com Express — tipagem de req e res
// src/types/express.d.ts — extendendo os tipos do Express
import { Usuario } from "../models/Usuario";
declare global {
namespace Express {
interface Request {
usuario?: Usuario; // adicionado pelo middleware de auth
}
}
}
// src/controllers/authController.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { Usuario, IUsuario } from "../models/Usuario";
// Tipando o corpo da requisição
interface LoginBody {
email: string;
senha: string;
}
interface RegistroBody {
nome: string;
email: string;
senha: string;
}
// Função tipada — TypeScript sabe os tipos de tudo
export async function login(
req: Request<{}, {}, LoginBody>,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { email, senha } = req.body; // tipado como LoginBody
if (!email || !senha) {
res.status(400).json({ erro: "Email e senha são obrigatórios." });
return;
}
const usuario = await Usuario.findOne({ email }).select("+senha");
if (!usuario) {
res.status(401).json({ erro: "Credenciais inválidas." });
return;
}
const senhaCorreta = await usuario.verificarSenha(senha);
if (!senhaCorreta) {
res.status(401).json({ erro: "Credenciais inválidas." });
return;
}
const token = jwt.sign(
{ id: usuario._id },
process.env.JWT_SECRET as string,
{ expiresIn: "7d" }
);
res.json({ token, usuario });
} catch (erro) {
next(erro);
}
}
// Tipando parâmetros de rota
interface ParamsComId {
id: string;
}
export async function buscarPorId(
req: Request<ParamsComId>,
res: Response,
next: NextFunction
): Promise<void> {
try {
const { id } = req.params; // tipado como string
const usuario = await Usuario.findById(id);
if (!usuario) {
res.status(404).json({ erro: "Usuário não encontrado." });
return;
}
res.json(usuario);
} catch (erro) {
next(erro);
}
}
Tipos para o Mongoose
// src/models/Usuario.ts
import mongoose, { Document, Model, Schema } from "mongoose";
import bcrypt from "bcrypt";
// Interface do documento
export interface IUsuario extends Document {
nome: string;
email: string;
senha: string;
ativo: boolean;
createdAt: Date;
updatedAt: Date;
// Métodos de instância
verificarSenha(senha: string): Promise<boolean>;
}
// Interface do Model (métodos estáticos)
interface IUsuarioModel extends Model<IUsuario> {
buscarPorEmail(email: string): Promise<IUsuario | null>;
}
const usuarioSchema = new Schema<IUsuario, IUsuarioModel>(
{
nome: { type: String, required: true },
email: { type: String, required: true, unique: true },
senha: { type: String, required: true, select: false },
ativo: { type: Boolean, default: true },
},
{ timestamps: true }
);
usuarioSchema.pre("save", async function (next) {
if (!this.isModified("senha")) return next();
this.senha = await bcrypt.hash(this.senha, 12);
next();
});
usuarioSchema.methods.verificarSenha = async function (
this: IUsuario,
senha: string
): Promise<boolean> {
return bcrypt.compare(senha, this.senha);
};
usuarioSchema.statics.buscarPorEmail = function (
email: string
): Promise<IUsuario | null> {
return this.findOne({ email }).select("+senha");
};
export const Usuario = mongoose.model<IUsuario, IUsuarioModel>(
"Usuario",
usuarioSchema
);
Boas práticas com TypeScript
// ✅ 1. Prefira interfaces para objetos públicos
// Prefira types para unions e aliases
interface Config { porta: number; dbUrl: string; }
type Ambiente = "development" | "production" | "test";
// ✅ 2. Evite any — use unknown quando o tipo é incerto
function parsearJSON(texto: string): unknown {
return JSON.parse(texto); // retorna unknown, não any
}
// ✅ 3. Use as const para objetos e arrays imutáveis
const ROTAS = {
LOGIN: "/auth/login",
REGISTRO: "/auth/registrar",
} as const;
// ROTAS.LOGIN tem o tipo "/auth/login" (literal), não string
// ✅ 4. Non-null assertion (!) — use com cuidado
const elemento = document.querySelector("#app")!; // sabe que existe
// ✅ 5. Type guards — verificações de tipo em runtime
function ehString(valor: unknown): valor is string {
return typeof valor === "string";
}
function ehUsuario(valor: unknown): valor is IUsuario {
return (
typeof valor === "object" &&
valor !== null &&
"nome" in valor &&
"email" in valor
);
}
// ✅ 6. Nunca ignore erros do TypeScript com @ts-ignore
// @ts-ignore ← ❌ esconde o problema
// @ts-expect-error ← melhor: documenta que é intencional
// ✅ 7. Ative strict: true no tsconfig — nunca desabilite
Tarefa para você
Migre a API REST do Módulo 4 para TypeScript:
// 1. Configure o tsconfig.json e instale as dependências:
// npm install -D typescript @types/node @types/express
// npm install -D @types/bcrypt @types/jsonwebtoken
// npm install -D ts-node-dev
// 2. Renomeie todos os arquivos .js para .ts
// 3. Crie interfaces para todos os modelos:
// IUsuario, ITarefa — com todos os campos e métodos
// 4. Crie um arquivo src/types/index.ts com:
// - Todos os tipos utilitários do projeto
// - UsuarioPublico (omite senha)
// - TarefaComUsuario (populate)
// - RespostaAPI<T> genérica
// - ErroValidacao
// 5. Tipe todos os controllers:
// - req.body com interfaces específicas
// - req.params com tipos corretos
// - Valores de retorno explícitos
// 6. Crie um enum para Status da Tarefa e Prioridade
// 7. Compile com npm run build e corrija todos os erros
// (sem usar any como atalho!)
// 8. Adicione ao tsconfig: "noImplicitAny": true
// e veja o TypeScript te ajudar a encontrar pontos não tipados
Ver solução — a migração completa — tsconfig, tipos, models, middleware e controller
// ---- tsconfig.json
// {
// "compilerOptions": {
// "target": "ES2022",
// "module": "node18",
// "rootDir": "src",
// "outDir": "dist",
// "esModuleInterop": true,
// "forceConsistentCasingInFileNames": true,
//
// "strict": true, // liga tudo abaixo de uma vez
// "noImplicitAny": true, // 8 — explícito, apesar de já vir no strict
// "strictNullChecks": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
//
// "skipLibCheck": true, // não valide os .d.ts das dependências
// "sourceMap": true
// },
// "include": ["src/**/*.ts"],
// "exclude": ["node_modules", "dist"]
// }
//
// scripts:
// "build": "tsc"
// "typecheck": "tsc --noEmit"
// "dev": "ts-node-dev --respawn src/index.ts"
// ---- src/types/enums.ts
// 6 — os enums
export enum StatusTarefa {
Pendente = "pendente",
EmProgresso = "em_progresso",
Concluida = "concluida",
Cancelada = "cancelada",
}
export enum Prioridade {
Baixa = "baixa",
Media = "media",
Alta = "alta",
}
// Enum de string, e não numérico: o valor gravado no Mongo é "pendente", que
// se lê num dump. Enum numérico gravaria 0, e um dia alguém insere um status
// no meio da lista e renumera silenciosamente o banco inteiro.
// ---- src/models/Usuario.ts
// 3 — as interfaces dos modelos
import mongoose, { Schema, Model, HydratedDocument } from "mongoose";
import bcrypt from "bcryptjs";
export interface IUsuario {
nome: string;
email: string;
senha: string;
ativo: boolean;
createdAt: Date;
updatedAt: Date;
}
// Métodos ficam numa interface separada: IUsuario descreve o DOCUMENTO CRU,
// que é o que sai de um .lean() e o que entra num create().
export interface IUsuarioMetodos {
verificarSenha(senhaDigitada: string): Promise<boolean>;
}
export type UsuarioDoc = HydratedDocument<IUsuario, IUsuarioMetodos>;
type UsuarioModel = Model<IUsuario, Record<string, never>, IUsuarioMetodos>;
const usuarioSchema = new Schema<IUsuario, UsuarioModel, IUsuarioMetodos>(
{
nome: { type: String, required: true, trim: true, minlength: 2 },
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
match: [/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i, "Email inválido."],
},
senha: { type: String, required: true, minlength: 6, select: false },
ativo: { type: Boolean, default: true },
},
{ timestamps: true, versionKey: false }
);
// Sem o parâmetro `next`: em hook async, o Mongoose 9 passa um objeto de
// opções no lugar dele, e `next()` viraria "next is not a function".
usuarioSchema.pre("save", async function () {
if (!this.isModified("senha")) return;
this.senha = await bcrypt.hash(this.senha, 12);
});
// O `this: UsuarioDoc` explícito é o que faz `this.senha` ter tipo aqui
// dentro — em function comum, o TypeScript não adivinha o dono do método.
usuarioSchema.methods.verificarSenha = function (
this: UsuarioDoc,
senhaDigitada: string
): Promise<boolean> {
return bcrypt.compare(senhaDigitada, this.senha);
};
export const Usuario = mongoose.model<IUsuario, UsuarioModel>("Usuario", usuarioSchema);
// ---- src/models/Tarefa.ts
import mongoose, { Schema, Types, HydratedDocument } from "mongoose";
import { StatusTarefa, Prioridade } from "../types/enums";
export interface ITarefa {
titulo: string;
descricao: string;
status: StatusTarefa;
prioridade: Prioridade;
prazo: Date | null;
tags: string[];
usuario: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
// Virtuais também ficam à parte: `atrasada` não existe no banco, e incluí-la
// em ITarefa faria o create() exigir um campo que ninguém pode passar.
export interface ITarefaVirtuais {
atrasada: boolean;
}
export type TarefaDoc = HydratedDocument<ITarefa, ITarefaVirtuais>;
const tarefaSchema = new Schema<ITarefa>(
{
titulo: { type: String, required: true, trim: true, maxlength: 200 },
descricao: { type: String, trim: true, maxlength: 2000, default: "" },
status: {
type: String,
enum: Object.values(StatusTarefa),
default: StatusTarefa.Pendente,
},
prioridade: {
type: String,
enum: Object.values(Prioridade),
default: Prioridade.Media,
},
prazo: { type: Date, default: null },
tags: { type: [String], default: [] },
usuario: { type: Schema.Types.ObjectId, ref: "Usuario", required: true },
},
{ timestamps: true, versionKey: false }
);
tarefaSchema.index({ usuario: 1, status: 1 });
tarefaSchema.virtual("atrasada").get(function (this: ITarefa): boolean {
if (!this.prazo || this.status === StatusTarefa.Concluida) return false;
return new Date() > this.prazo;
});
tarefaSchema.set("toJSON", { virtuals: true });
export const Tarefa = mongoose.model<ITarefa>("Tarefa", tarefaSchema);
// ---- src/types/index.ts
// 4 — os tipos utilitários do projeto
import { Request } from "express";
import { Types } from "mongoose";
import { IUsuario, UsuarioDoc } from "../models/Usuario";
import { ITarefa } from "../models/Tarefa";
import { StatusTarefa, Prioridade } from "./enums";
export { StatusTarefa, Prioridade };
/** O usuário como ele pode sair na resposta HTTP: sem senha, com id string. */
export type UsuarioPublico = Omit<IUsuario, "senha"> & { id: string };
/** Tarefa depois do populate("usuario") — o ObjectId virou objeto. */
export type TarefaComUsuario = Omit<ITarefa, "usuario"> & {
_id: Types.ObjectId;
usuario: UsuarioPublico;
};
/**
* Envelope de toda resposta. É união discriminada de propósito: com `ok`
* como discriminante, quem consome só alcança `dados` depois de checar
* `if (resposta.ok)`. O compilador cobra o tratamento do erro.
*/
export type RespostaAPI<T> =
| { ok: true; dados: T; paginacao?: Paginacao }
| { ok: false; erro: string; detalhes?: string[] };
export interface Paginacao {
total: number;
pagina: number;
por_pagina: number;
total_paginas: number;
}
export interface ErroValidacao {
campo: string;
mensagem: string;
valorRecebido?: unknown; // unknown, nunca any: obriga a estreitar antes de usar
}
/** Requisição que já passou pelo middleware `autenticar`. */
export interface RequisicaoAutenticada<
Corpo = unknown,
Params extends Record<string, string> = Record<string, string>,
Query = unknown,
> extends Request<Params, unknown, Corpo, Query> {
usuario: UsuarioDoc;
}
export interface CorpoCriarTarefa {
titulo: string;
descricao?: string;
status?: StatusTarefa;
prioridade?: Prioridade;
prazo?: string | null; // string: JSON não tem Date
tags?: string[];
}
export interface QueryListarTarefas {
status?: StatusTarefa;
prioridade?: Prioridade;
busca?: string;
pagina?: string; // string: query string SEMPRE chega como texto
por_pagina?: string;
ordenar?: string;
}
// ---- src/middlewares/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt, { JwtPayload } from "jsonwebtoken";
import { Usuario } from "../models/Usuario";
import { RequisicaoAutenticada } from "../types";
interface PayloadToken extends JwtPayload {
id: string;
}
function segredo(): string {
const valor = process.env.JWT_SECRET;
// process.env é string | undefined. Sem esta checagem,
// jwt.verify(token, undefined) só falharia em produção.
if (!valor) throw new Error("JWT_SECRET não configurado.");
return valor;
}
export async function autenticar(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({ ok: false, erro: "Token de autenticação ausente." });
return; // `return res.status(...)` não compila: o retorno é Promise<void>
}
let payload: PayloadToken;
try {
payload = jwt.verify(authHeader.slice(7), segredo()) as PayloadToken;
} catch (erro) {
// `erro` é unknown no catch. instanceof estreita sem cast.
const expirado = erro instanceof jwt.TokenExpiredError;
res.status(401).json({
ok: false,
erro: expirado ? "Token expirado. Faça login novamente." : "Token inválido.",
});
return;
}
const usuario = await Usuario.findById(payload.id);
if (!usuario || !usuario.ativo) {
res.status(401).json({ ok: false, erro: "Usuário não encontrado ou inativo." });
return;
}
(req as RequisicaoAutenticada).usuario = usuario;
next();
}
// ---- src/controllers/tarefaController.ts
// 5 — controllers tipados: corpo, params, query e retorno
import { Response, NextFunction } from "express";
import { QueryFilter } from "mongoose";
import { Tarefa, ITarefa } from "../models/Tarefa";
import {
RequisicaoAutenticada,
RespostaAPI,
CorpoCriarTarefa,
QueryListarTarefas,
} from "../types";
type RespostaListar = Response<RespostaAPI<ITarefa[]>>;
type RespostaTarefa = Response<RespostaAPI<ITarefa>>;
export async function listar(
req: RequisicaoAutenticada<unknown, Record<string, string>, QueryListarTarefas>,
res: RespostaListar,
next: NextFunction
): Promise<void> {
try {
const { status, prioridade, busca, pagina, por_pagina, ordenar } = req.query;
const filtros: QueryFilter<ITarefa> = { usuario: req.usuario._id };
if (status) filtros.status = status;
if (prioridade) filtros.prioridade = prioridade;
if (busca) filtros.titulo = { $regex: busca, $options: "i" };
const limite = Math.min(Number(por_pagina) || 10, 50);
const paginaAtual = Math.max(Number(pagina) || 1, 1);
const [tarefas, total] = await Promise.all([
Tarefa.find(filtros)
.sort(ordenar ?? "-createdAt")
.skip((paginaAtual - 1) * limite)
.limit(limite)
.lean<ITarefa[]>(), // sem o genérico, lean() devolve o tipo cru
Tarefa.countDocuments(filtros),
]);
res.json({
ok: true,
dados: tarefas,
paginacao: {
total,
pagina: paginaAtual,
por_pagina: limite,
total_paginas: Math.ceil(total / limite),
},
});
} catch (erro) {
next(erro);
}
}
export async function criar(
req: RequisicaoAutenticada<CorpoCriarTarefa>,
res: RespostaTarefa,
next: NextFunction
): Promise<void> {
try {
const { titulo, descricao, status, prioridade, prazo, tags } = req.body;
const tarefa = await Tarefa.create({
titulo,
descricao,
status,
prioridade,
prazo: prazo ? new Date(prazo) : null,
tags,
usuario: req.usuario._id,
});
res.status(201).json({ ok: true, dados: tarefa.toObject() });
} catch (erro) {
next(erro);
}
}
// ---- verificacao.ts
// 7 e 8 — `npx tsc --noEmit` no projeto acima: sai limpo, sem um `any`.
//
// Para ver o compilador trabalhando, um arquivo com os erros clássicos:
//
// export function resumir(tarefas) {
// return tarefas.map((t) => t.titulo);
// }
//
// export async function primeiroTitulo(): Promise<string> {
// const tarefa = await Tarefa.findOne();
// return tarefa.titulo;
// }
//
// export function ehFinal(status: StatusTarefa): boolean {
// return status === "arquivada";
// }
//
// A saída:
//
// src/demo-erros.ts(4,25): error TS7006: Parameter 'tarefas' implicitly has an 'any' type.
// src/demo-erros.ts(5,23): error TS7006: Parameter 't' implicitly has an 'any' type.
// src/demo-erros.ts(10,10): error TS18047: 'tarefa' is possibly 'null'.
// src/demo-erros.ts(14,10): error TS2367: This comparison appears to be unintentional
// because the types 'StatusTarefa' and '"arquivada"' have no overlap.
//
// O TS18047 é o que paga a migração sozinho: `findOne()` devolve
// `TarefaDoc | null`, e o "Cannot read properties of null" que você só veria
// em produção — no dia em que o id não existir — vira erro de compilação.
// O TS2367 é o segundo: um status escrito errado deixa de ser um `if` que
// nunca entra e passa a ser um build que não passa.
//
// Ao migrar de verdade, dois nomes mudaram no Mongoose 9 e valem o aviso:
// `FilterQuery` virou `QueryFilter`, e hook async não recebe mais `next`.
A tentação, no meio da migração, é calar o compilador com any — e aí o projeto fica com a sintaxe do TypeScript e a segurança do Javascript. Quando o tipo é mesmo desconhecido, o certo é unknown: ele obriga a estreitar antes de usar, em vez de liberar tudo. E o ponto que passa despercebido: o TypeScript não valida nada em tempo de execução. req.body é CorpoCriarTarefa porque você declarou, não porque alguém conferiu — o cliente ainda pode mandar o que quiser. Tipo é contrato com o compilador; na fronteira da API, continua sendo preciso validar de verdade.
TypeScript não roda: ele é apagado na compilação, e o que sobra é o JavaScript de sempre. Daí decorrem duas consequências que orientam todo o resto — nenhum tipo protege contra o que chega da rede em tempo de execução, o que mantém necessária a validação da resposta de uma API; e o any não é um tipo, é a desistência de ter um, apagando a verificação justamente onde ela seria mais útil.
Fontes e Referências
- TypeScript — Documentação oficial: https://www.typescriptlang.org/docs
- TypeScript — Handbook: https://www.typescriptlang.org/docs/handbook/intro.html
- TypeScript — Utility Types: https://www.typescriptlang.org/docs/handbook/utility-types.html
- DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped
- ts-node: https://typestrong.org/ts-node
- Matt Pocock — Total TypeScript: https://www.totaltypescript.com
- Programming TypeScript — Boris Cherny (O'Reilly)
- Effective TypeScript — Dan Vanderkam (O'Reilly)
- roadmap.sh — TypeScript: https://roadmap.sh/typescript
Exercícios
Exercício 1
Esta é a função sem tipos. O que cada chamada devolve — e qual delas é a mais perigosa?
function calcularDesconto(preco, percentual) {
return preco - (preco * percentual / 100);
}
calcularDesconto("100", 10); // A
calcularDesconto("100,50", 10); // B
calcularDesconto(100, "dez"); // C
Ver resposta
✓ Resposta: A devolve 90, B e C devolvem NaN — e a perigosa é a A, justamente porque funciona. Os operadores - e * convertem os operandos em número antes de operar; só o + concatena quando um dos lados é string. Então uma string numérica atravessa a função inteira e produz o resultado certo, o que faz o teste passar, a tela mostrar o valor correto e ninguém desconfiar de nada. O defeito só aparece quando a entrada muda de forma — um preço com vírgula decimal, vindo de um formulário brasileiro ou de um CSV, e a conta vira NaN, que se propaga em silêncio por todo o cálculo até chegar ao usuário como "R$ NaN". B e C são os casos honestos: falham cedo e de forma visível. A moral vale para além deste exemplo: coerção implícita não é perigosa por dar erro, é perigosa por não dar — ela adia a falha até um ponto onde a causa já não é rastreável. É esse adiamento que o TypeScript elimina, recusando a chamada no editor, antes de existir execução.
Exercício 2
O código compila sem nenhum erro e quebra em produção com TypeError: usuario.nome.toUpperCase is not a function. Como isso é possível, se o tipo estava declarado?
interface Usuario {
id: number;
nome: string;
idade: number;
}
const resposta = await fetch("/api/usuarios/1");
const usuario = (await resposta.json()) as Usuario;
console.log(usuario.nome.toUpperCase());
Ver resposta
✓ Resposta: Porque o tipo não existe em tempo de execução. Todo o sistema de tipos é apagado na compilação — o que roda é JavaScript puro, sem nenhuma verificação. E o as Usuario não checa coisa alguma: ele é uma afirmação, uma forma de dizer ao compilador "confie em mim, é disto que se trata". Se a API devolver { "nome": null }, ou { "name": "Ana" } em inglês, ou um objeto de erro, o compilador continua satisfeito e o programa quebra na primeira propriedade acessada. Essa é a fronteira que todo projeto TypeScript precisa ter clara: dentro do código, os tipos garantem; na borda — rede, arquivo, formulário, banco, variável de ambiente — eles não garantem nada. A solução é validar de verdade no ponto de entrada, com um type guard escrito à mão ou, na prática, com uma biblioteca de esquema como o Zod, que faz a checagem em tempo de execução e deriva o tipo estático a partir do mesmo esquema — uma declaração só, válida nos dois mundos. Vale ainda a distinção entre as duas ferramentas: as silencia o compilador, unknown mais um type guard obriga a provar. A primeira é conveniência; a segunda é segurança.
Exercício 3
Duas funções recebem dados de fora. Por que a segunda é considerada correta e a primeira, uma desistência?
function processarA(dados: any) {
return dados.usuario.perfil.nome.trim();
}
function processarB(dados: unknown) {
return dados.usuario.perfil.nome.trim();
}
Ver resposta
✓ Resposta: A primeira compila e a segunda não — e é exatamente esse o ponto. Com any, o TypeScript desliga toda verificação naquele valor: a cadeia inteira de propriedades é aceita sem perguntas, e se qualquer elo do caminho for undefined, o erro aparece só em produção. Pior, any é contagioso: o retorno de processarA também é any, e ele contamina tudo o que tocar adiante, o que faz um único any mal colocado abrir um buraco em boa parte do projeto. Já unknown diz "não sei o que é isto, e você também não" — qualquer acesso é recusado até que o tipo seja estreitado, com typeof, in, instanceof ou um type guard. A versão correta da segunda função é verificar antes de usar. A regra prática: any é a renúncia a ter tipo, unknown é a admissão de que ainda não se sabe qual é — e o segundo é sempre o que se quer para dado externo. Quando o any for mesmo inevitável, vale isolá-lo numa função pequena, converter o resultado para um tipo real ali dentro e nunca deixá-lo vazar para o resto.
Exercício 4
O tsconfig.json tem "strict": false. O código abaixo compila. Onde ele quebra em execução?
function saudar(usuario: { nome: string }) {
return `Olá, ${usuario.nome.toUpperCase()}`;
}
const encontrado = usuarios.find(u => u.id === 999);
saudar(encontrado);
Ver resposta
✓ Resposta: Quebra na primeira linha da função, com Cannot read properties of undefined (reading 'toUpperCase'). O find devolve Usuario | undefined, porque pode não encontrar nada — e com strict desligado, mais precisamente com strictNullChecks desligado, o TypeScript trata null e undefined como valores válidos de qualquer tipo. A verificação some, a chamada é aceita, e o TypeScript deixa de proteger contra a classe de erro mais comum que existe em JavaScript, que é justamente acessar propriedade de algo que não veio. Com strict: true, o compilador recusa a chamada e obriga a tratar o caso: um if (encontrado), um encontrado?.nome, ou um valor padrão. É por isso que a recomendação do artigo — ativar strict e nunca desligar — não é preciosismo: sem ele, boa parte do benefício de adotar TypeScript simplesmente não existe. E a ressalva prática para quem está migrando um projeto antigo: ligar strict de uma vez costuma produzir centenas de erros. O caminho é ligar as verificações uma a uma, começando por strictNullChecks, que é a que dá o maior retorno, e deixar noImplicitAny para depois.
Exercício 5
O enum abaixo é usado para validar a entrada de uma API. Que valor inesperado passa na verificação?
enum Status {
Pendente, // 0
Ativo, // 1
Inativo, // 2
}
function atualizar(status: Status) {
console.log("status:", status);
}
atualizar(Status.Ativo); // ok
atualizar(7); // ?
Ver resposta
✓ Resposta: Em versões anteriores à 5.0 do TypeScript, atualizar(7) era aceito: enums numéricos permitiam qualquer número, por causa da compatibilidade com o uso de enum como conjunto de flags combináveis por operações de bit. Da 5.0 em diante isso passou a ser erro para enums sem valores calculados, mas o histórico explica por que o padrão caiu em desuso. E há outras características desconfortáveis: o enum numérico gera um objeto em tempo de execução com mapeamento reverso — Status[1] devolve "Ativo" —, o que faz Object.keys(Status) retornar seis entradas em vez de três e quebra qualquer iteração ingênua; e o valor que trafega na API vira 0, 1, 2, números sem significado no banco e no log, que desalinham para sempre se alguém inserir um item no meio da lista. A alternativa moderna é a union de literais — type Status = "pendente" | "ativo" | "inativo" —, que não gera nada em tempo de execução, recusa qualquer valor fora da lista e grava texto legível. Quando for preciso iterar sobre as opções, o padrão é declarar o array com as const e derivar o tipo dele com typeof LISTA[number], mantendo uma fonte única. O enum de string, que o artigo também apresenta, é um meio-termo aceitável: não tem mapeamento reverso e grava texto, mas ainda existe em execução.