Javascript

Introdução ao TypeScript Já leu

14 min de leitura

Introdução ao TypeScript
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

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
Comentários

Mais em Javascript

Temporizadores: setTimeout e setInterval
Temporizadores: setTimeout e setInterval

At&eacute; agora todo o nosso c&oacute;digo executou imediatamente &mdash; um...

Revisão  + Projeto Final: Produção Real
Revisão + Projeto Final: Produção Real

Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança...

Condicionais: if, else e switch
Condicionais: if, else e switch

No Javascript n&oacute;s guardamos informa&ccedil;&otilde;es em vari&aacute;v...