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); // "10010" 😱 concatenação de string!
calcularDesconto(100, "dez"); // NaN 😱
calcularDesconto(); // NaN 😱 sem argumento, sem aviso
// ── 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 — mais eficiente (inline na compilação)
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 é "'/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
Conclusão
Neste artigo você aprendeu:
- Por que TypeScript existe e o problema que resolve
- Como instalar e configurar o TypeScript para projetos Node.js
- Tipos primitivos, arrays, tuplas, any e unknown
- Interfaces — definindo contratos para objetos e classes
- Type aliases e a diferença prática em relação a interfaces
- Generics — código reutilizável com segurança de tipos
- Utility Types — Partial, Omit, Pick, Record e outros prontos para usar
- Enums e a alternativa moderna com union literals
- Classes com modificadores de acesso, getters, setters e herança
- Como tipar o Express — Request, Response e extensões globais
- Como tipar o Mongoose com interfaces de Document e Model
- Boas práticas — avoid any, type guards, as const e strict mode
No próximo artigo encerramos o Módulo 5 com uma revisão completa de qualidade de código e um projeto que aplica testes, ESLint, Prettier, Git semântico e TypeScript juntos.
📚 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