TypeScript

Sistema de Tipos — Structural Typing vs Nominal Typing

TypeScript usa structural typing (duck typing estático): dois tipos são compatíveis se suas estruturas são compatíveis, independente do nome.

interface Point2D {
  x: number;
  y: number;
}

interface Vector2D {
  x: number;
  y: number;
}

// Point2D e Vector2D são o MESMO tipo para o TypeScript — mesma estrutura
const point: Point2D = { x: 1, y: 2 };
const vector: Vector2D = point; // OK — estruturalmente compatíveis

// Excess property checking — exceção importante
// Objetos literais passam por checagem EXTRA (detecta typos)
const p: Point2D = { x: 1, y: 2, z: 3 }; // ERRO: 'z' não existe em Point2D
const obj = { x: 1, y: 2, z: 3 };
const p2: Point2D = obj; // OK — sem excess property check em variáveis intermediárias

Type Inference — O Compilador é Mais Esperto do Que Você Pensa

// O TS infere tipos em quase todos os contextos — não anote o óbvio
const name = "Lucas";          // tipo: "Lucas" (literal type, não string)
let name2 = "Lucas";           // tipo: string (let permite reatribuição)
const names = ["Ana", "Bob"];  // tipo: string[]
const tuple = [1, "a"] as const; // tipo: readonly [1, "a"]

// Return type inference — deixe o TS inferir
function add(a: number, b: number) {
  return a + b; // TS infere: number
}

// Satisfies — valida o tipo SEM alargar ou estreitar
const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;

// palette.green ainda é string (não string | number[])
palette.green.toUpperCase(); // OK — TS sabe que green é string
// Sem satisfies, Record<string, string | number[]> perderia essa informação

// const assertions — congela o tipo no nível mais estreito possível
const ROUTES = {
  HOME: "/",
  ADMIN: "/admin",
  PROFILE: "/profile",
} as const;
// tipo: { readonly HOME: "/"; readonly ADMIN: "/admin"; readonly PROFILE: "/profile" }
type Route = (typeof ROUTES)[keyof typeof ROUTES]; // "/" | "/admin" | "/profile"

Type Narrowing — Refinamento Progressivo de Tipos

// O TS estreita tipos automaticamente em control flow

function process(value: string | number | null | undefined) {
  // Truthiness narrowing
  if (!value) return; // tipo aqui: string | number (null e undefined eliminados)

  // typeof narrowing
  if (typeof value === "string") {
    value.toUpperCase(); // tipo: string
    return;
  }

  value.toFixed(2); // tipo: number (string já foi eliminado acima)
}

// in operator narrowing
interface Fish { swim(): void }
interface Bird { fly(): void }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // tipo: Fish
  } else {
    animal.fly();  // tipo: Bird
  }
}

// instanceof narrowing
function formatDate(input: string | Date) {
  if (input instanceof Date) {
    return input.toISOString(); // tipo: Date
  }
  return new Date(input).toISOString(); // tipo: string
}

// Assignment narrowing
let x: string | number;
x = "hello";
x.toUpperCase(); // tipo: string (TS rastreia a última atribuição)

Utility Types — O Canivete Suíço do Dia a Dia

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user" | "viewer";
  createdAt: Date;
  settings?: { theme: "light" | "dark"; notifications: boolean };
}

// Partial<T> — torna TODAS as propriedades opcionais
type UpdateUserDTO = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required<T> — torna TODAS as propriedades obrigatórias
type StrictUser = Required<User>;
// settings deixa de ser opcional

// Pick<T, K> — seleciona apenas as chaves listadas
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, K> — remove as chaves listadas
type CreateUserDTO = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: ...; settings?: ... }

// Record<K, V> — cria objeto com chaves K e valores V
type RolePermissions = Record<User["role"], string[]>;
// { admin: string[]; user: string[]; viewer: string[] }

// Exclude<T, U> — remove membros de union que são assignable a U
type NonAdmin = Exclude<User["role"], "admin">;
// "user" | "viewer"

// Extract<T, U> — mantém apenas membros assignable a U
type AdminOnly = Extract<User["role"], "admin" | "moderator">;
// "admin"

// NonNullable<T> — remove null e undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

// ReturnType<T> — extrai o tipo de retorno de uma função
function fetchUser(id: number) {
  return { id, name: "Lucas", email: "lucas@example.com" };
}
type FetchUserResult = ReturnType<typeof fetchUser>;
// { id: number; name: string; email: string }

// Parameters<T> — extrai os tipos dos parâmetros como tuple
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: number]

// Awaited<T> — unwrap de Promise (recursivo)
type AwaitedUser = Awaited<Promise<Promise<User>>>;
// User (resolve todas as camadas de Promise)

Generics — Tipos Parametrizados

// Generics são "funções para tipos" — recebem parâmetros de tipo

// Sem generic: precisa de overload ou any
function firstElement(arr: any[]): any { return arr[0]; }

// Com generic: preserva a informação de tipo
function firstElement<T>(arr: T[]): T | undefined { return arr[0]; }
const n = firstElement([1, 2, 3]);    // tipo: number | undefined
const s = firstElement(["a", "b"]);   // tipo: string | undefined

// Constraints (extends) — restringe quais tipos são aceitos
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user: User = { id: 1, name: "Lucas", email: "", role: "admin", createdAt: new Date() };
getProperty(user, "name");  // tipo: string
getProperty(user, "xyz");   // ERRO: '"xyz"' não é assignable a keyof User

// Generic com default
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  timestamp: Date;
}
type GenericResponse = ApiResponse;         // data: unknown
type UserResponse = ApiResponse<User>;      // data: User
type UsersResponse = ApiResponse<User[]>;   // data: User[]

Conditional Types — Lógica no Nível de Tipos

// T extends U ? X : Y — "se T é assignable a U, então X, senão Y"

type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

// Distributive conditional types — distribui sobre unions automaticamente
type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>; // string[] | number[]
// O TS aplica a condição para cada membro da union separadamente

// Prevenir distribuição com colchetes:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type D = ToArrayNonDist<string | number>; // (string | number)[]

// infer — extrair tipo dentro de um conditional
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type E = UnwrapPromise<Promise<string>>; // string
type F = UnwrapPromise<number>;          // number (não é Promise, retorna T)

// infer com constraints (TS 4.7+)
type FirstIfString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type G = FirstIfString<["hello", 42]>; // "hello"
type H = FirstIfString<[42, "hello"]>; // never (primeiro elemento não é string)

// Exemplo prático: extrair tipos de parâmetros de rota
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// { userId: string } & { postId: string }

Template Literal Types — Manipulação de Strings no Nível de Tipos

// Combinação de literal types cria todas as permutações
type Color = "red" | "green" | "blue";
type Shade = "light" | "dark";
type ColorVariant = `${Shade}-${Color}`;
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"

// Built-in string manipulation types
type Upper = Uppercase<"hello">;     // "HELLO"
type Lower = Lowercase<"HELLO">;     // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"

// Pattern matching em strings
type ExtractDomain<T extends string> = T extends `${string}@${infer Domain}` ? Domain : never;
type Domain = ExtractDomain<"lucas@example.com">; // "example.com"

// Converter snake_case para camelCase no nível de tipos
type CamelCase<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Lowercase<Head>}${Capitalize<CamelCase<Tail>>}`
    : Lowercase<S>;

type A2 = CamelCase<"user_first_name">; // "userFirstName"
type B2 = CamelCase<"created_at">;      // "createdAt"

// Aplicar recursivamente em todas as chaves de um objeto
type CamelCaseKeys<T> = {
  [K in keyof T as K extends string ? CamelCase<K> : K]: T[K]
};

interface SnakeCaseUser {
  user_id: number;
  first_name: string;
  created_at: Date;
}

type CamelUser = CamelCaseKeys<SnakeCaseUser>;
// { userId: number; firstName: string; createdAt: Date }

Discriminated Unions — Modelagem de Estados com Segurança

// Tagged union: cada variante tem um campo literal discriminante

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error; retryCount: number };

function renderState(state: RequestState<User[]>) {
  switch (state.status) {
    case "idle":
      return null;
    case "loading":
      return <Spinner />;
    case "success":
      return state.data.map((u) => <UserCard key={u.id} user={u} />);
      // TS sabe que state.data existe porque status === "success"
    case "error":
      return <ErrorBanner message={state.error.message} retries={state.retryCount} />;
  }
}

// Exhaustive checking com never — garante que todos os casos são tratados
function assertNever(value: never): never {
  throw new Error(`Caso não tratado: ${JSON.stringify(value)}`);
}

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape);
      // Se alguém adicionar um novo kind (ex: "polygon") e esquecer o case,
      // o TS dá erro: 'Shape & { kind: "polygon" }' não é assignable a 'never'
  }
}

// Discriminated union para eventos de domínio
type DomainEvent =
  | { type: "USER_CREATED"; payload: { userId: string; email: string } }
  | { type: "USER_UPDATED"; payload: { userId: string; changes: Partial<User> } }
  | { type: "USER_DELETED"; payload: { userId: string; reason: string } }
  | { type: "ORDER_PLACED"; payload: { orderId: string; items: OrderItem[] } };

// Type-safe event handler map
type EventHandler<T extends DomainEvent["type"]> = (
  payload: Extract<DomainEvent, { type: T }>["payload"]
) => void;

const handlers: { [T in DomainEvent["type"]]?: EventHandler<T> } = {
  USER_CREATED: (payload) => {
    // payload é tipado como { userId: string; email: string }
    sendWelcomeEmail(payload.email);
  },
  ORDER_PLACED: (payload) => {
    // payload é tipado como { orderId: string; items: OrderItem[] }
    processOrder(payload.orderId, payload.items);
  },
};

Type Guards — Narrowing Customizado

// Custom type predicate: paramName is Type
interface Cat { meow(): void; purr(): void }
interface Dog { bark(): void; fetch(): void }

// O retorno "animal is Cat" diz ao TS que, se retornar true, o argumento é Cat
function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function interact(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.purr(); // tipo: Cat
  } else {
    animal.fetch(); // tipo: Dog
  }
}

// Type guard com assertion: asserts paramName is Type
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new TypeError(`Esperado string, recebido ${typeof value}`);
  }
}

function processInput(input: unknown) {
  assertIsString(input);
  // Daqui para frente, TS sabe que input é string
  console.log(input.toUpperCase());
}

// Type guard para discriminated unions com array filter
const events: DomainEvent[] = [/* ... */];
const userEvents = events.filter(
  (e): e is Extract<DomainEvent, { type: `USER_${string}` }> =>
    e.type.startsWith("USER_")
);
// userEvents é tipado como array de eventos USER_*

// Type guard genérico reutilizável
function isNotNullish<T>(value: T): value is NonNullable<T> {
  return value !== null && value !== undefined;
}

const mixed = [1, null, 2, undefined, 3];
const numbers = mixed.filter(isNotNullish);
// tipo: number[] (não (number | null | undefined)[])

Mapped Types — Transformação Sistemática de Tipos

// Mapped type básico: [K in keyof T]
type Readonly2<T> = { readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };  // Remove readonly
type Concrete<T> = { [K in keyof T]-?: T[K] };          // Remove optional

// Key remapping com as (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }

// Filtrar chaves com conditional em as
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
};

type StringFields = OnlyStrings<User>;
// { name: string; email: string } — só campos cujo valor é string

// DeepReadonly — recursivo
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
};

// DeepPartial — recursivo
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepPartial<T[K]>
    : T[K]
};

// Exemplo prático: tipo para form state com validação
type FormState<T> = {
  values: T;
  errors: { [K in keyof T]?: string };
  touched: { [K in keyof T]?: boolean };
  isValid: boolean;
  isDirty: boolean;
};

type UserFormState = FormState<CreateUserDTO>;

Declaration Merging e Module Augmentation

// Interface merging — interfaces com mesmo nome são combinadas
interface Window {
  analytics: AnalyticsClient;  // Adiciona ao Window global
}
window.analytics.track("page_view"); // Sem erro

// Module augmentation — estender tipos de bibliotecas externas
// Arquivo: types/express.d.ts
declare module "express-serve-static-core" {
  interface Request {
    user?: AuthenticatedUser;     // Adiciona user ao Request do Express
    requestId: string;
  }
}

// Agora em qualquer handler Express:
app.get("/profile", (req, res) => {
  req.user?.name;     // Tipado corretamente
  req.requestId;      // Tipado corretamente
});

// Augmentação de módulo genérico (ex: estender Zod)
declare module "zod" {
  interface ZodString {
    cpf(): ZodString;  // Adiciona método .cpf() ao ZodString
  }
}

// Ambient declarations — declarar tipos para módulos sem tipagem
declare module "*.svg" {
  const content: React.FC<React.SVGAttributes<SVGElement>>;
  export default content;
}

declare module "*.css" {
  const classes: Record<string, string>;
  export default classes;
}

// Global augmentation dentro de um módulo
export {}; // Torna o arquivo um módulo

declare global {
  interface Array<T> {
    groupBy<K extends string>(fn: (item: T) => K): Record<K, T[]>;
  }
}

Variance — Covariância e Contravariância

// Variância determina a compatibilidade de tipos em posição genérica

// Covariância (out): Dog extends Animal → Container<Dog> extends Container<Animal>
// Arrays são covariantes em TS (por design, não é sound)
class Animal { name = "animal"; }
class Dog extends Animal { bark() {} }

const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // OK — covariante (mas unsound!)
// animals.push(new Animal()); // Compila, mas quebra em runtime — é um Dog[]

// Contravariância (in): se Dog extends Animal, então
// (animal: Animal) => void extends (dog: Dog) => void? NÃO!
// É o contrário: (dog: Dog) => void NÃO É assignable a (animal: Animal) => void

// Com strictFunctionTypes: true (ESSENCIAL — ative sempre)
type Handler<T> = (value: T) => void;
const animalHandler: Handler<Animal> = (a) => console.log(a.name);
const dogHandler: Handler<Dog> = (d) => d.bark();

// animalHandler é assignable a dogHandler? Sim, porque Animal é mais genérico.
const h: Handler<Dog> = animalHandler; // OK
// dogHandler é assignable a animalHandler? Não! Dog tem bark(), Animal não.
// const h2: Handler<Animal> = dogHandler; // ERRO com strictFunctionTypes

// Annotation explícita de variância (TS 4.7+)
interface Producer<out T> {  // Covariante: T só aparece em posição de output
  get(): T;
}

interface Consumer<in T> {   // Contravariante: T só aparece em posição de input
  accept(value: T): void;
}

interface Transformer<in I, out O> {  // Misto
  transform(input: I): O;
}

Branded Types — Simulando Tipos Nominais

// Problema: string é string — não distingue UserId de OrderId
function getUser(id: string) { /* ... */ }
function getOrder(id: string) { /* ... */ }
getUser(orderId); // Compila sem erro, mas é um bug lógico!

// Solução: Branded types (Opaque types)
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;

// Funções construtoras que validam
function userId(id: string): UserId {
  if (!id.startsWith("usr_")) throw new Error("UserId inválido");
  return id as UserId;
}

function orderId(id: string): OrderId {
  if (!id.startsWith("ord_")) throw new Error("OrderId inválido");
  return id as OrderId;
}

function email(value: string): Email {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) throw new Error("Email inválido");
  return value as Email;
}

// Agora o compilador impede mistura
function getUserById(id: UserId) { /* ... */ }
function getOrderById(id: OrderId) { /* ... */ }

const uid = userId("usr_123");
const oid = orderId("ord_456");

getUserById(uid);  // OK
getUserById(oid);  // ERRO: OrderId não é assignable a UserId
getUserById("usr_123"); // ERRO: string não é assignable a UserId

// Branded types para valores numéricos
type Cents = Brand<number, "Cents">;
type Dollars = Brand<number, "Dollars">;

function cents(value: number): Cents { return value as Cents; }
function centsToLDollars(c: Cents): Dollars { return (c / 100) as Dollars; }
// Previne mistura acidental de centavos com dólares

Performance do Compilador — Projetos Grandes

# Project References — dividir monorepo em sub-projetos
# tsconfig.json (raiz)
{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

# packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,      // Requerido para project references
    "declaration": true,    // Gera .d.ts
    "declarationMap": true, // Go-to-definition funciona entre projetos
    "outDir": "./dist"
  }
}

# Build incremental: tsc --build (ou tsc -b)
# Só recompila pacotes que mudaram + dependentes transitivos.

# Incremental builds (projeto único)
{
  "compilerOptions": {
    "incremental": true,    // Salva estado em .tsbuildinfo
    "tsBuildInfoFile": "./.tsbuildinfo"
  }
}
# Primeira build: 30s → Builds seguintes: 2-3s (só recompila o que mudou)

# isolatedModules — requerido para transpiladores (esbuild, swc, Babel)
{
  "compilerOptions": {
    "isolatedModules": true
    // Cada arquivo deve ser compilável isoladamente.
    // Proíbe: const enums, namespace merging, re-export de types sem "type"
  }
}

# Dicas para performance:
# 1. Use "skipLibCheck": true (não checa node_modules/.d.ts)
# 2. Exclua node_modules e dist do include
# 3. Evite tipos recursivos profundos (> 5 níveis)
# 4. Prefira interfaces a type aliases (interfaces são cacheadas por nome)
# 5. Use project references em monorepos

tsconfig Deep Dive — Opções que Importam

{
  "compilerOptions": {
    // === Strict Mode (SEMPRE ative tudo) ===
    "strict": true,              // Ativa TODAS as flags strict de uma vez:
    // "noImplicitAny": true,    //   Erro se tipo inferido é any
    // "strictNullChecks": true,  //   null/undefined são tipos separados
    // "strictFunctionTypes": true, // Contravariância em parâmetros de função
    // "strictBindCallApply": true, // Tipos corretos para bind/call/apply
    // "strictPropertyInitialization": true, // Classes: propriedades devem ser inicializadas
    // "noImplicitThis": true,   //   Erro se this é implicitamente any
    // "alwaysStrict": true,     //   Emite "use strict" em todos os arquivos
    // "useUnknownInCatchVariables": true, // catch(e) → e é unknown, não any

    // === Extra safety (não incluídas no strict) ===
    "noUncheckedIndexedAccess": true,  // array[0] é T | undefined, não T
    "exactOptionalProperties": true,   // { a?: string } não aceita { a: undefined }
    "noPropertyAccessFromIndexSignature": true, // Força obj["key"] para index signatures

    // === Module Resolution ===
    "module": "ESNext",
    "moduleResolution": "bundler",  // Para projetos com Vite/webpack/esbuild
    // "moduleResolution": "node16", // Para bibliotecas publicadas ou Node.js puro
    // "moduleResolution": "nodenext", // Alias para node16 (segue versão do Node)

    // bundler vs node16:
    //   bundler: permite import sem extensão, resolve index.ts, package.json "exports"
    //   node16: exige extensões (.js!), resolve "exports" com condições, ESM strict

    // === Path Aliases ===
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],           // import { db } from "@/lib/db"
      "@components/*": ["./src/components/*"],
      "@utils/*": ["./src/utils/*"]
    },
    // IMPORTANTE: paths é APENAS para o TS. O bundler precisa de configuração separada:
    // Vite: resolve.alias no vite.config.ts
    // webpack: resolve.alias no webpack.config.js

    // === Output ===
    "target": "ES2022",            // Alvo de compilação (determina quais features são transpiladas)
    "lib": ["ES2022", "DOM", "DOM.Iterable"], // APIs disponíveis em runtime
    "jsx": "react-jsx",            // React 17+ transform (não precisa de import React)
    "outDir": "./dist",
    "declaration": true,           // Gera .d.ts (necessário para libs)
    "sourceMap": true,

    // === Imports ===
    "esModuleInterop": true,       // import fs from "fs" ao invés de import * as fs
    "allowImportingTsExtensions": true, // Permite import "./foo.ts" (com bundler)
    "resolveJsonModule": true,     // Permite import config from "./config.json"
    "isolatedModules": true,       // Compatibilidade com esbuild/swc
    "verbatimModuleSyntax": true,  // import type DEVE usar "import type { X }"
                                    // Substitui importsNotUsedAsValues
    "skipLibCheck": true           // Pula checagem de .d.ts (performance)
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Patterns Avançados — Builder, Result, Exhaustive Switch

Builder Pattern com Tipos Acumulativos

// O tipo acumula as propriedades configuradas — garante que build() só é chamado
// quando todas as obrigatórias foram setadas.

type BuilderState = {
  host: boolean;
  port: boolean;
  database: boolean;
};

class DatabaseConfigBuilder<State extends BuilderState = { host: false; port: false; database: false }> {
  private config: Partial<{ host: string; port: number; database: string }> = {};

  host(value: string): DatabaseConfigBuilder<State & { host: true }> {
    this.config.host = value;
    return this as any;
  }

  port(value: number): DatabaseConfigBuilder<State & { port: true }> {
    this.config.port = value;
    return this as any;
  }

  database(value: string): DatabaseConfigBuilder<State & { database: true }> {
    this.config.database = value;
    return this as any;
  }

  // build() só está disponível quando TODAS as propriedades são true
  build(
    this: DatabaseConfigBuilder<{ host: true; port: true; database: true }>
  ): { host: string; port: number; database: string } {
    return this.config as any;
  }
}

const config = new DatabaseConfigBuilder()
  .host("localhost")
  .port(5432)
  .database("myapp")
  .build(); // OK

// new DatabaseConfigBuilder().host("localhost").build();
// ERRO: 'build' não existe quando port e database são false

Result Type — Tratamento de Erros sem Exceções

// Inspirado em Rust: Result<T, E> força o chamador a tratar o erro

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

// Uso: parsing de configuração
type ConfigError =
  | { code: "MISSING_FIELD"; field: string }
  | { code: "INVALID_VALUE"; field: string; expected: string }
  | { code: "FILE_NOT_FOUND"; path: string };

function parseConfig(raw: unknown): Result<AppConfig, ConfigError> {
  if (typeof raw !== "object" || raw === null) {
    return err({ code: "INVALID_VALUE", field: "root", expected: "object" });
  }

  const obj = raw as Record<string, unknown>;

  if (!obj.port) {
    return err({ code: "MISSING_FIELD", field: "port" });
  }

  if (typeof obj.port !== "number" || obj.port < 1 || obj.port > 65535) {
    return err({ code: "INVALID_VALUE", field: "port", expected: "number (1-65535)" });
  }

  // ... mais validações

  return ok({
    port: obj.port,
    host: (obj.host as string) ?? "localhost",
  });
}

// O chamador é OBRIGADO a checar o resultado
const result = parseConfig(rawConfig);
if (!result.ok) {
  switch (result.error.code) {
    case "MISSING_FIELD":
      console.error(`Campo obrigatório ausente: ${result.error.field}`);
      break;
    case "INVALID_VALUE":
      console.error(`Valor inválido para ${result.error.field}: esperado ${result.error.expected}`);
      break;
    case "FILE_NOT_FOUND":
      console.error(`Arquivo não encontrado: ${result.error.path}`);
      break;
  }
  process.exit(1);
}

// Aqui o TS sabe que result.ok === true
console.log(`Servidor na porta ${result.value.port}`);

Exhaustive Switch com Mapa de Handlers

// Pattern para garantir que toda ação é tratada, com tipo de retorno correto

type Action =
  | { type: "INCREMENT"; amount: number }
  | { type: "DECREMENT"; amount: number }
  | { type: "RESET" }
  | { type: "SET"; value: number };

// Tipo helper que exige handler para cada Action["type"]
type ActionHandlerMap<State> = {
  [A in Action as A["type"]]: (state: State, action: A) => State;
};

interface CounterState {
  count: number;
  lastAction: Action["type"] | null;
}

// Se você esquecer de tratar qualquer tipo de ação, o TS dá erro
const handlers: ActionHandlerMap<CounterState> = {
  INCREMENT: (state, action) => ({
    count: state.count + action.amount,  // action tipada como { type: "INCREMENT"; amount: number }
    lastAction: "INCREMENT",
  }),
  DECREMENT: (state, action) => ({
    count: state.count - action.amount,
    lastAction: "DECREMENT",
  }),
  RESET: (state) => ({
    count: 0,
    lastAction: "RESET",
  }),
  SET: (state, action) => ({
    count: action.value,  // action tipada como { type: "SET"; value: number }
    lastAction: "SET",
  }),
};

function reducer(state: CounterState, action: Action): CounterState {
  const handler = handlers[action.type] as (state: CounterState, action: Action) => CounterState;
  return handler(state, action);
}

Zod + TypeScript — Schema como Fonte de Verdade

import { z } from "zod";

// O schema Zod é a single source of truth — o tipo TS é derivado
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  email: z.string().email(),
  role: z.enum(["admin", "user", "viewer"]),
  createdAt: z.coerce.date(), // Aceita string ISO e converte para Date
  settings: z.object({
    theme: z.enum(["light", "dark"]).default("light"),
    notifications: z.boolean().default(true),
  }).optional(),
});

// Tipo derivado do schema — NUNCA defina manualmente se tem Zod
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "user" | "viewer"; ... }

// Schema para criação (omite id e createdAt)
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
type CreateUserDTO = z.infer<typeof CreateUserSchema>;

// Schema para update (tudo parcial)
const UpdateUserSchema = CreateUserSchema.partial();
type UpdateUserDTO = z.infer<typeof UpdateUserSchema>;

// Validação em runtime com tipo seguro
function createUser(input: unknown): Result<User, z.ZodError> {
  const parsed = CreateUserSchema.safeParse(input);
  if (!parsed.success) return err(parsed.error);

  const user: User = {
    ...parsed.data,
    id: crypto.randomUUID(),
    createdAt: new Date(),
  };

  return ok(user);
}

Type-Level Programming

TypeScript permite computação no nível de tipos — tipos que geram outros tipos recursivamente.

Tipos Recursivos

// Deep Readonly: torna todas as propriedades (inclusive nested) readonly
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type Config = DeepReadonly<{
  db: { host: string; port: number };
  features: { darkMode: boolean };
}>;
// Config.db.host é readonly — não pode ser reatribuído

// DeepPartial: torna tudo nested como optional
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// Path types: extrair todas as paths possíveis de um objeto
type Path<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]: K | `${K}.${Path<T[K], "">}`;
    }[keyof T & string]
  : never;

type UserPaths = Path<{ name: string; address: { street: string; city: string } }>;
// "name" | "address" | "address.street" | "address.city"

Branded Types

Pattern para criar tipos nominais — strings e números que são semanticamente diferentes.

// Sem branded types: qualquer string pode ser usada como UserId ou Email
type UserId_Bad = string;
type Email_Bad = string;

// Com branded types: incompatíveis por construção
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type Email = Brand<string, "Email">;
type OrderId = Brand<string, "OrderId">;

// Funções construtoras com validação
function createUserId(id: string): UserId {
  if (!id.startsWith("usr_")) throw new Error("Invalid user ID format");
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!email.includes("@")) throw new Error("Invalid email");
  return email as Email;
}

// Agora o compilador impede erros:
function getUser(id: UserId): Promise<User> { /* ... */ }

const userId = createUserId("usr_abc123");
const email = createEmail("test@test.com");

getUser(userId);   // ✓ OK
// getUser(email);  // ✗ COMPILE ERROR: Email is not assignable to UserId
// getUser("abc");  // ✗ COMPILE ERROR: string is not assignable to UserId

Satisfies Operator

satisfies verifica que uma expressão satisfaz um tipo sem alargar o tipo inferido.

type Route = { path: string; component: string };
type Routes = Record<string, Route>;

// Com `as const`: perde a verificação de tipo
// Com type annotation: perde o tipo literal
// Com `satisfies`: melhor dos dois mundos!

const routes = {
  home: { path: "/", component: "HomePage" },
  about: { path: "/about", component: "AboutPage" },
  blog: { path: "/blog", component: "BlogPage" },
} satisfies Routes;

// routes.home.path é inferido como "/" (literal), não string
// MAS: routes.invalid geraria erro se adicionado (precisa satisfazer Routes)

// Comparação:
// const x: Routes = { ... }     → x.home.path: string (perde literais)
// const x = { ... } as Routes   → sem verificação (pode ter erros)
// const x = { ... } satisfies Routes → x.home.path: "/" (mantém literais + verifica)

Module Augmentation Avançado

Extender tipos de bibliotecas de terceiros sem forkar:

// Extender tipos do Express para adicionar user autenticado
declare module "express-serve-static-core" {
  interface Request {
    user?: {
      id: string;
      email: string;
      role: "admin" | "user";
    };
  }
}

// Extender tipos do processo Node.js para variáveis de ambiente
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      JWT_SECRET: string;
      NODE_ENV: "development" | "production" | "test";
      PORT?: string;
    }
  }
}

// Agora process.env.DATABASE_URL é tipado como string (não string | undefined)
// E process.env.TYPO geraria erro de compilação

Referencias e Fontes

  • TypeScript Official Documentationhttps://www.typescriptlang.org/docs — Documentacao oficial com referencia completa do sistema de tipos, configuracao do compilador e guias de migracao
  • “Programming TypeScript” — Boris Cherny — Livro que cobre desde fundamentos ate padroes avancados de tipos, incluindo tipos condicionais, mapped types e type-level programming
  • TypeScript Handbookhttps://www.typescriptlang.org/docs/handbook — Guia pratico e progressivo que ensina os conceitos fundamentais e avancados do TypeScript com exemplos aplicados
  • Type Challengeshttps://github.com/type-challenges/type-challenges — Exercícios práticos de type-level programming