Primeiros Passos
Hello World & Compilação
Arquivos TypeScript usam a extensão .ts. O compilador tsc transpila TS para JS, apagando todas as anotações de tipo em tempo de execução. Use --strict para máxima type safety. ts-node ou bun podem executar arquivos .ts diretamente sem uma etapa de compilação separada.
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);
// Compile to JavaScript (type annotations erased):
// tsc hello.ts -> hello.js
// tsc --strict hello.ts // enable all strict checks
// tsc --watch hello.ts // recompile on change
// Run directly with ts-node or bun:
// ts-node hello.tstsconfig.json
tsconfig.json configura o compilador TypeScript. 'strict: true' habilita noImplicitAny, strictNullChecks, strictFunctionTypes e mais. 'target' controla a versão JS de saída. 'esModuleInterop' habilita default imports de módulos CommonJS como os built-ins do Node.
{
"compilerOptions": {
"target": "ES2020", // JS version to emit
"module": "ESNext", // module system
"strict": true, // enable all strict checks
"outDir": "./dist", // output directory
"rootDir": "./src", // source root
"esModuleInterop": true, // allow default imports from CJS
"skipLibCheck": true, // skip .d.ts type checking
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Anotações de Tipo & Inferência
Anotações de tipo especificam explicitamente o tipo de uma variável. TypeScript também pode inferir tipos a partir de valores. Use anotações explícitas para assinaturas de função e APIs públicas; confie na inferência para variáveis locais. Evite 'any' — ele opta out da verificação de tipos inteiramente.
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];
// Type inference (let TS figure it out)
let age = 30; // inferred as number
let items = [1, 2, 3]; // inferred as number[]
let mixed = [1, "a"]; // inferred as (string | number)[]
// any disables type checking (avoid!)
let anything: any = 42;
anything = "string"; // no errorVerificações do Strict Mode
Strict mode habilita verificações críticas: strictNullChecks (null/undefined não atribuíveis a outros tipos), noImplicitAny (parâmetros devem ter tipos), strictPropertyInitialization (campos de classe devem ser inicializados). Use '!' (definite assignment) quando tiver certeza de que um field será definido depois.
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode
// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any
// strictPropertyInitialization
class User {
name: string; // Error: not initialized
constructor() {}
}
// Fix: initialize or use definite assignment
class Fixed {
name!: string; // definite assignment assertion
age: number = 0;
}Arquivos de Declaração (.d.ts)
Arquivos de declaração (.d.ts) fornecem tipos para bibliotecas JavaScript sem definições TypeScript. 'declare' diz ao compilador que uma variável/função existe em tempo de execução. Use pacotes @types do DefinitelyTyped para bibliotecas populares (ex.: @types/node, @types/react).
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
export function greet(name: string): string;
export const version: string;
}
// global.d.ts - extend global scope
declare global {
interface Window {
myApp: { version: string };
}
}
// Usage in .ts files:
// import { greet } from "my-lib"; // now typed
// Install @types packages: npm i -D @types/node @types/reactTipos Básicos
Primitivos & Tipos Especiais
Primitivos do TypeScript: string, number, boolean, bigint, symbol. 'void' indica que uma função não retorna valor. 'never' representa valores que nunca ocorrem — funções que lançam ou rodam para sempre. Use 'never' para verificações exaustivas em switch statements.
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");
// void: function returns nothing
function log(msg: string): void { console.log(msg); }
// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }Arrays & Tuples
Arrays usam tanto a sintaxe T[] quanto Array<T>. ReadonlyArray previne mutações. Tuples são arrays de comprimento fixo com tipos específicos em cada índice — úteis para pares chave-valor ou dados tipo CSV. Labeled tuples melhoram legibilidade com posições nomeadas.
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];
// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable
// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1]; // number
// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];Enums
Enums definem um conjunto de constantes nomeadas. String enums são recomendadas para depuração (valores são legíveis na saída). Numeric enums suportam reverse mapping. 'const enum' é apagado em tempo de compilação (inlined) para zero custo de runtime. Prefira union types para casos simples.
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up; // 0
// String enum (recommended for readability)
enum Status {
Pending = "PENDING",
Success = "SUCCESS",
Failed = "FAILED",
}
// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"
// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;any vs unknown vs never
'any' desabilita toda verificação de tipo — evite-o. 'unknown' é a alternativa type-safe: você deve estreitá-lo (via typeof, instanceof) antes do uso. 'never' representa valores que nunca ocorrem, usado para exhaustiveness checking em switch statements para capturar cases ausentes em tempo de compilação.
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string"; // OK
a.toUpperCase(); // OK (no check, may fail at runtime)
// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
u.toUpperCase(); // OK after narrowing
}
// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
switch (s) {
case "circle": return Math.PI;
case "square": return 1;
default:
const _exhaustive: never = s; // Error if case missing
}
}Type Assertions
Type assertions dizem ao compilador 'confie em mim, eu sei o tipo'. Use a sintaxe 'as'. A non-null assertion (!) diz ao TS que um valor não é null/undefined. 'as const' torna todas as propriedades readonly literals — útil para objetos de config e tipos de action do Redux. Assertions não mudam comportamento de runtime.
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;
// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi"; // el is HTMLElement, not null
// Double assertion (for unsafe casts)
let value = "42" as unknown as number;
// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonlyTipos Literal & Union
Tipos literal restringem um valor a uma string, number ou boolean específico. Combinados com unions, criam tipos precisos como direction ou métodos HTTP. Template literal types (TS 4.1+) constroem tipos de string a partir de outros tipos — poderoso para gerar chaves type-safe e nomes de evento.
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left"; // OK
// direction = "sideways"; // Error
// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4; // OK
// Boolean literal
let flag: true = true;
// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"Interfaces & Objetos
Básico de Interfaces
Interfaces descrevem a forma de objetos. '?' marca propriedades opcionais (podem ser undefined). 'readonly' previne reatribuição após inicialização. Interfaces são apenas em tempo de compilação — são apagadas no JavaScript de saída. Use-as para definir contratos para objetos e classes.
interface User {
id: number;
name: string;
email?: string; // optional property
readonly createdAt: Date; // immutable
}
const user: User = {
id: 1,
name: "Alice",
createdAt: new Date(),
};
// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefinedIndex Signatures
Index signatures permitem objetos com chaves arbitrárias de um dado tipo. Todos os valores de propriedade devem ser atribuíveis ao tipo de index. Útil para dicionários, caches e dados dinâmicos. Combine com propriedades conhecidas para configs tipadas com opções extras.
// Index signature: arbitrary string keys
interface StringMap {
[key: string]: string;
}
const dict: StringMap = {
name: "Alice",
city: "NYC",
// count: 42, // Error: value must be string
};
// Mixed: known + index signature
interface Config {
name: string;
[key: string]: string | number;
}
// Readonly index signature
interface ReadonlyMap {
readonly [key: string]: number;
}Estendendo Interfaces
Interfaces podem estender uma ou mais outras interfaces, combinando seus membros. Isso habilita composição e reuso de código. Diferente de classes, interfaces suportam herança múltipla. Quando você implementa uma interface, a classe deve fornecer todos os membros necessários.
interface Animal {
name: string;
eat(): void;
}
interface Pet extends Animal {
owner: string;
play(): void;
}
interface Swimmer {
swim(): void;
}
// Multiple inheritance
interface Duck extends Pet, Swimmer {
quack(): void;
}
const duck: Duck = {
name: "Donald",
owner: "Walt",
eat() {},
play() {},
swim() {},
quack() {},
};Function Types em Interfaces
Interfaces podem descrever assinaturas de função, habilitando callbacks type-safe. Hybrid interfaces (callable + properties) são usadas para funções estilo jQuery que também têm métodos. Esse padrão é comum em bibliotecas que retornam funções com helpers anexados.
interface SearchFn {
(source: string, keyword: string): boolean;
}
const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true
// Interface with mixed members (hybrid)
interface Counter {
(start: number): void; // callable
count: number; // property
reset(): void; // method
}
// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };Interface vs Type Alias
Interfaces suportam declaration merging (interfaces de mesmo nome se combinam), melhores mensagens de erro e são preferidas para formas de objeto/classe. Type aliases são mais flexíveis (podem representar unions, primitives, tuples), mas não podem ser merged. Use interfaces para APIs extensíveis, type aliases para unions e tipos computados.
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };
// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;
// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };
// Use interface for objects/classes, type for unions/aliasesOptional Chaining & Nullish Coalescing
Optional chaining (?.) acessa propriedades aninhadas com segurança — retorna undefined em vez de lançar se qualquer link for null/undefined. Nullish coalescing (??) fornece um default apenas para null/undefined (não 0 ou ''). Esses operadores reduzem dramaticamente código verboso de null-checking.
interface User {
profile?: {
address?: {
city?: string;
};
};
}
const user: User = {};
// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined
// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";
// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky
// Optional method call
const result = user.profile?.address?.city?.toUpperCase();Type Aliases & Unions
Type Aliases
Type aliases criam referências nomeadas a qualquer tipo, incluindo unions, intersections, primitives e generics. Diferente de interfaces, aliases não podem ser merged ou extended, mas são mais flexíveis. Use aliases para unions, tuples e utility types; use interfaces para formas de objeto.
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };
// Generic alias
type Container<T> = { value: T };
// Function type alias
type Handler<T> = (event: T) => void;
// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);Union Types
Union types (A | B) permitem que um valor seja de um de vários tipos. TypeScript estreita o tipo dentro de blocos condicionais usando typeof, instanceof ou in checks. Nota: (string | number)[] é diferente de string[] | number[] — o primeiro é um array misto, o último é all-strings OU all-numbers.
// Union: value can be one of several types
type ID = string | number;
function display(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // narrowed to string
} else {
console.log(id.toFixed(2)); // narrowed to number
}
}
display("abc"); // ABC
display(42); // 42.00
// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];Intersection Types
Intersection types (A & B) combinam todos os membros de múltiplos tipos — o resultado deve satisfazer todo tipo. Útil para mixins, composição e merging de utility types. Diferente de union (OR), intersection é AND: o valor deve ter todas as propriedades de todos os tipos.
// Intersection: combine multiple types into one
interface BusinessPartner {
name: string;
credit: number;
}
interface Identity {
id: number;
email: string;
}
type Employee = BusinessPartner & Identity;
const emp: Employee = {
name: "Alice",
credit: 1000,
id: 1,
email: "[email protected]",
};
// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing propsNullable Types
Em strict mode, null e undefined não são atribuíveis a outros tipos — você deve incluí-los explicitamente com unions (string | null). Parâmetros opcionais (param?) são implicitamente T | undefined. Use ?? para defaults seguros e ! para assert non-null (use com parcimônia).
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice"; // OK
// Optional parameters are implicitly | undefined
function greet(name?: string) {
// name is string | undefined
console.log(name ?? "Guest");
}
// Return type can be null
function find(id: number): string | null {
return id === 1 ? "Alice" : null;
}
// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"Operadores Keyof & Typeof
'keyof T' extrai as chaves do tipo T como um string literal union. 'typeof x' extrai o tipo de um valor (útil para inferir a partir de objetos). 'keyof typeof obj' combina ambos para obter as chaves de um objeto existente — comum em tipos de action do Redux e accessors de propriedade type-safe.
interface User {
id: number;
name: string;
email: string;
}
// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"
function getProp(obj: User, key: keyof User) {
return obj[key];
}
// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }
// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"Mapped Types
Mapped types iteram sobre chaves para transformar um tipo. Built-in utilities como Readonly, Partial e Pick são mapped types. Use modificadores + e - para adicionar/remover readonly ou optional. Key remapping (TS 4.1+) renomeia chaves usando template literal types — poderoso para gerar tipos getter/setter.
// Map over keys to create a new type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User { id: number; name: string; }
type ReadonlyUser = Readonly<User>; // all readonly
type OptionalUser = Optional<User>; // all optional
// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };
// Key remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};Funções
Function Types & Signatures
TypeScript adiciona anotações de tipo a parâmetros de função e valores de retorno. O tipo de retorno pode frequentemente ser inferido, mas anotação explícita é recomendada para APIs públicas. Parâmetros padrão tornam argumentos opcionais com um valor de fallback. Use void quando uma função não retorna nada.
// Named function with types
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;
// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;
// Void return (no return value)
function log(msg: string): void { console.log(msg); }
// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
return `${greeting}, ${name}!`;
}
greet("Alice"); // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"Rest Parameters & Tuples
Rest parameters (...args) coletam múltiplos argumentos em um array. TypeScript os tipa como T[] ou um tuple para funções variadic de comprimento fixo. O operador spread (...) faz o reverso — expande um array em argumentos individuais. Tuple rest types habilitam assinaturas variadic precisas.
// Rest parameters (variadic)
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
console.log(name, scores);
}
// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));
// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
const [s, n, b] = args;
}Function Overloads
Function overloads fornecem múltiplas assinaturas de tipo para a mesma função, habilitando tipos de retorno precisos com base na entrada. A assinatura de implementação é oculta de chamadores. Overloads são resolvidos top-down — coloque assinaturas mais específicas primeiro. Comum em bibliotecas como jQuery e lodash.
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
if (typeof input === "string") {
return input.split(",");
}
return [input, input * 2];
}
const strs = parse("a,b,c"); // string[]
const nums = parse(42); // number[]
// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
return m === undefined
? new Date(yOrTs)
: new Date(yOrTs, m - 1, d);
}Tipo this
TypeScript permite declarar o tipo 'this' como o primeiro parâmetro. Isso garante que a função seja chamada com o contexto correto — útil para métodos passados como callbacks. Arrow functions capturam 'this' lexicalmente, evitando a necessidade de .bind(). Use 'noImplicitThis' para capturar erros de 'this' não tipado.
interface Card {
suit: string;
rank: string;
isFaceUp(): boolean;
}
// Explicit 'this' parameter
function format(this: Card): string {
return `${this.rank} of ${this.suit}`;
}
const card: Card = {
suit: "Hearts",
rank: "A",
isFaceUp() { return true; },
format,
};
// 'this' in callbacks with bind
class Handler {
private count = 0;
increment = () => { this.count++; }; // arrow binds this
}Callbacks & Higher-Order Functions
TypeScript tipa totalmente higher-order functions (funções que recebem ou retornam funções). Use type parameters genéricos (T, U) para preservar relações de tipo entre entrada e saída. Callback types são comumente definidos como type aliases para reuso. Currying (retornar funções) é totalmente type-safe.
// Function as parameter
type Callback<T> = (value: T, index: number) => void;
function forEach<T>(arr: T[], cb: Callback<T>): void {
for (let i = 0; i < arr.length; i++) {
cb(arr[i], i);
}
}
forEach(["a", "b"], (v, i) => console.log(i, v));
// Function returning function (curry)
function add(a: number): (b: number) => number {
return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
return arr.map(fn);
}Parameter Destructuring
TypeScript suporta destructuring em parâmetros de função — anote a forma destructured inline ou via uma interface. Extrair para uma interface melhora legibilidade e reuso. Array/tuple destructuring também funciona. Esse padrão é comum em props de componentes React e handlers de API.
// Destructured parameters with types
function createUser({ name, age, email }: {
name: string;
age: number;
email?: string;
}): void {
console.log(name, age, email);
}
createUser({ name: "Alice", age: 30 });
// Extract to interface for reuse
interface UserOpts {
name: string;
age: number;
email?: string;
}
function updateUser({ name, age }: UserOpts): void {}
// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
return [b, a];
}Classes & OOP
Classe & Construtor
Classes TypeScript suportam parameter properties — prefixar params de construtor com access modifiers (public/private/protected/readonly) auto-cria e atribui fields. Esse shorthand reduz boilerplate. Métodos podem ter anotações de tipo em valores de retorno. Fields são public por padrão.
class Person {
// Parameter properties (shorthand)
constructor(
public name: string, // auto-creates this.name
private age: number, // private field
readonly id: number, // immutable
) {}
greet(): string {
return `Hi, I'm ${this.name}`;
}
}
const p = new Person("Alice", 30, 1);
console.log(p.name); // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonlyAccess Modifiers
Access modifiers: public (padrão, em todo lugar), private (apenas classe), protected (classe + subclasses), readonly (imutável). 'private' do TypeScript é apenas em tempo de compilação; private fields ES '#' são verdadeiramente privados em runtime. Use private para detalhes de implementação, protected para extension points.
class BankAccount {
public owner: string; // accessible everywhere
private balance: number; // class only
protected rate: number; // class + subclasses
readonly id: string; // immutable after init
#secret: string; // ES private (runtime)
constructor(owner: string) {
this.owner = owner;
this.balance = 0;
this.rate = 0.05;
this.id = crypto.randomUUID();
this.#secret = "hidden";
}
deposit(amount: number): void {
this.balance += amount;
}
}Herança & Classes Abstratas
Classes abstratas não podem ser instanciadas diretamente — elas definem uma base para subclasses. Abstract methods não têm implementação na classe base; subclasses devem implementá-los. Use 'extends' para herança e 'super()' para chamar o construtor pai. Classes abstratas habilitam polimorfismo — código pode funcionar com qualquer subclasse de Shape.
abstract class Shape {
constructor(public color: string) {}
abstract area(): number; // must be implemented
describe(): string {
return `${this.color} shape, area ${this.area()}`;
}
}
class Circle extends Shape {
constructor(color: string, private r: number) {
super(color);
}
area(): number { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
constructor(color: string, private side: number) {
super(color);
}
area(): number { return this.side ** 2; }
}
const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstractInterfaces & Implements
Uma classe pode implementar múltiplas interfaces (separadas por vírgulas). A classe deve fornecer todos os membros da interface. Diferente de extends (herança única), implements suporta múltiplos contratos. Essa é a forma do TypeScript de alcançar comportamento similar a herança múltipla. Use interfaces para definir contratos, classes para implementá-los.
interface Printable {
toString(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Printable, Comparable<Money> {
constructor(private amount: number) {}
toString(): string {
return `$${this.amount.toFixed(2)}`;
}
compareTo(other: Money): number {
return this.amount - other.amount;
}
}
const a = new Money(10);
const b = new Money(20);
console.log(a.toString()); // "$10.00"
console.log(a.compareTo(b)); // -10Getters & Setters
Getters e setters interceptam acesso a propriedade para validação, computação ou side effects. Use um private backing field (convenção: prefixo underscore). Getters habilitam propriedades computadas (como fahrenheit a partir de celsius). Setters habilitam validação. Acesse-os como propriedades regulares — sem parênteses.
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) throw new Error("Below absolute zero");
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * 5 / 9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throwsStatic Members & Singletons
Membros estáticos pertencem à classe, não a instâncias — acessados via ClassName.member. Use static para constantes, funções utilitárias e factory methods. Um construtor privado + static getInstance() implementa o padrão Singleton. 'as const' torna arrays estáticos readonly com tipos literal.
class Logger {
static instance: Logger;
private logs: string[] = [];
private constructor() {} // prevent direct new
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;
log(msg: string): void {
this.logs.push(msg);
}
}
const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructorGenerics
Funções Genéricas
Generics (<T>) permitem escrever funções que funcionam com qualquer tipo enquanto preservam type safety. O type parameter T é um placeholder preenchido em tempo de chamada — tanto explicitamente (identity<number>) quanto inferido a partir de argumentos. Generics habilitam estruturas de dados e algoritmos type-safe reutilizáveis.
// Generic function: preserves type relationship
function identity<T>(value: T): T {
return value;
}
const n = identity<number>(42); // T = number, returns number
const s = identity("hi"); // T inferred as string
// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("Alice", 30); // [string, number]
// Generic with array
function first<T>(arr: T[]): T | undefined {
return arr[0];
}Classes Genéricas
Classes genéricas (<T>) criam contêineres type-safe. Cada instância trava um tipo específico — um Stack<number> apenas aceita números. Isso captura erros de tipo em tempo de compilação sem overhead de runtime (generics são apagados). Comum em coleções (Stack, Queue, Map) e wrappers reativos (Observable<T>).
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2
const strStack = new Stack<string>();
strStack.push("hello");Generic Constraints
Constraints (T extends SomeType) restringem quais tipos um generic pode aceitar. 'T extends HasLength' garante que T tenha uma propriedade 'length'. 'K extends keyof T' (keyof constraint) garante que uma chave exista em um objeto, retornando o tipo de valor correto. Constraints habilitam acesso a propriedade type-safe e chamadas de método em generics.
// Constraint: T must have a 'length' property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("hello"); // 5 (string has length)
logLength([1, 2, 3]); // 3 (array has length)
// logLength(42); // Error: number has no length
// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a keyDefault Type Parameters
Default type parameters fornecem um tipo de fallback quando nenhum é especificado. Útil para APIs com um caso comum (ex.: ApiResponse padrão para string). Defaults podem depender de parâmetros anteriores. Combine com constraints (T extends X = DefaultType) para generics opcionais type-safe.
// Default type parameter
interface ApiResponse<T = string> {
data: T;
status: number;
}
const r1: ApiResponse = { data: "hello", status: 200 }; // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 }; // T = number
// Multiple defaults
class Container<T = string, U = number> {
constructor(public a: T, public b: U) {}
}
const c1 = new Container("hi", 42); // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>
// Default with constraint
interface Box<T extends object = { id: number }> {
value: T;
}Interfaces & Tipos Genéricos
Interfaces genéricas e type aliases criam contratos type-safe reutilizáveis. Repository<T> abstrai acesso a dados com uma API consistente. Result<T, E> é um discriminated union para tratamento de erros sem exceções. Default type parameters (E = Error) reduzem boilerplate para casos comuns.
// Generic interface
interface Repository<T> {
findById(id: string): Promise<T>;
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Implement with concrete type
class UserRepo implements Repository<User> {
async findById(id: string): Promise<User> { /* ... */ }
async save(user: User): Promise<void> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}
// Generic type alias with conditional
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };Conditional Types
Conditional types (T extends U ? X : Y) são if-statements em nível de tipo. 'infer R' extrai um tipo de dentro de outro tipo (ex.: tipo de retorno de uma função). Conditional types distribuem sobre unions — ToArray<string | number> torna-se string[] | number[]. Built-in utilities como Exclude, Extract e NonNullable usam isso.
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
// Extract return type
type AsyncReturnType<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;Tipos Avançados
Utility Types
TypeScript fornece utility types integrados para transformações comuns: Partial (tudo opcional), Pick (selecionar chaves), Omit (excluir chaves), Record (mapa chave-valor), Required (remover opcional), ReturnType (retorno de função), Parameters (params de função como tuple). Esses eliminam definições de tipo repetitivas.
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };
// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;
// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;
// Record: key-value map
type UserMap = Record<string, User>;
// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;
// ReturnType: function return type
type R = ReturnType<() => string>; // string
// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]Template Literal Types
Template literal types (TS 4.1+) constroem tipos de string interpolando outros tipos. Combinados com unions, geram produtos cartesianos de strings. Capitalize/Uppercase transformam o caso. Use-os para nomes de evento type-safe, geração getter/setter e tipagem de rotas de API. A cláusula 'as' em mapped types habilita renomeação de chaves.
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">; // "Foo"
// Getter names from keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"Keyword infer
A keyword 'infer' declara uma variável de tipo dentro da cláusula 'extends' de um conditional type, capturando um tipo para reuso. É a base de utility types como ReturnType, Parameters e Awaited. Use infer para extrair tipos de estruturas complexas (arrays, promises, funções) sem decomô-las manualmente.
// Extract return type of a function
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean
// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string
// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number
// Extract first parameter
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // stringDiscriminated Unions
Discriminated unions (tagged unions) usam um campo literal compartilhado ('type', 'kind', 'tag') para distinguir variantes. TypeScript estreita o tipo em cada case de switch, dando acesso a campos específicos do case. O default 'never' habilita exhaustiveness checking — se você adicionar um novo case, o compilador erro até você tratá-lo. Essencial para reducers do Redux e state machines.
// Discriminated union: shared 'type' (or 'kind') field
type Action =
| { type: "ADD_TODO"; text: string }
| { type: "DELETE_TODO"; id: number }
| { type: "TOGGLE_TODO"; id: number };
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [...state, { id: Date.now(), text: action.text }];
case "DELETE_TODO":
return state.filter(t => t.id !== action.id);
case "TOGGLE_TODO":
return state.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
);
default:
const _: never = action; // exhaustiveness check
return state;
}
}Type Guards: typeof & instanceof
Type guards estreitam tipos em runtime. 'typeof' funciona para primitives (string, number, boolean, symbol, bigint, undefined, function, object). 'instanceof' verifica protótipos de class/constructor. Array.isArray() estreita para um array tipado. Esses são guards integrados — sem código personalizado necessário. TypeScript rastreia o tipo estreitado em cada branch.
// typeof: narrow primitives
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string
}
return value.toFixed(2); // number
}
// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }
function speak(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // Dog
} else {
pet.meow(); // Cat
}
}
// Array.isArray
function flatten(arr: (number | number[])[]) {
return arr.flatMap(x =>
Array.isArray(x) ? x : [x]
);
}Custom Type Guards & Operador in
O operador 'in' verifica se uma propriedade existe em um objeto, estreitando para o tipo que a tem. Type predicates (x is T) são funções guard personalizadas que retornam boolean, mas também estreitam o tipo. Use 'unknown' como tipo de entrada para parse seguro de dados externos (JSON.parse, respostas de API). Predicates habilitam verificações de tipo reutilizáveis e composáveis.
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// Type predicate: custom guard function
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: any): x is { name: string; age: number } {
return x && typeof x.name === "string" && typeof x.age === "number";
}
const val: unknown = "hello";
if (isString(val)) {
console.log(val.toUpperCase()); // narrowed to string
}Estruturas de Dados
Arrays & ReadonlyArray
Arrays TypeScript são tipados — métodos como map, filter e reduce preservam tipos de elemento. Use readonly T[] ou ReadonlyArray<T> para imutabilidade. Tuples têm comprimento fixo e posições tipadas. Array destructuring e spread são totalmente type-safe. O type system captura index-out-of-bounds e atribuições de tipo errado.
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];
// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly
// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];
// Array methods are typed
const doubled = nums.map(n => n * 2); // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number
// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];Objetos & Records
Record<K, V> cria um mapa tipado com chaves do tipo K e valores do tipo V. Partial<T> torna todas as propriedades opcionais — ideal para operações de update/patch. Object.entries/keys/values retornam arrays tipados. Use Pick e Omit para derivar tipos focados a partir de existentes, mantendo os tipos DRY.
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };
// Record: typed key-value map
const scores: Record<string, number> = {
math: 90,
science: 85,
};
// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };
// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;
// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores); // string[]
const values = Object.values(scores); // number[]Maps & Sets
Map e Set são coleções ES6 com suporte total a TypeScript. Chaves de Map podem ser de qualquer tipo (diferente de objetos, que coercem chaves para strings). Set armazena valores únicos. WeakMap/WeakSet permitem garbage collection de chaves — útil para metadata anexada a elementos DOM ou objetos sem impedir GC.
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined
// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true
// Iteration (typed)
for (const [key, value] of map) {
console.log(key, value.name);
}
for (const num of unique) {
console.log(num);
}
// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();Tuples & Labeled Tuples
Tuples são arrays de comprimento fixo com posições tipadas. Labeled tuples (TS 4.0+) adicionam nomes para legibilidade — úteis para valores de retorno e dados tipo CSV. Tuples habilitam múltiplos valores de retorno sem criar uma interface. Use 'readonly' para prevenir mutação. Tuples diferem de arrays: [string, number] NÃO é (string | number)[].
// Basic tuple
let point: [number, number] = [10, 20];
// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];
// Destructuring with labels
const [id, name, active] = user;
// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
return [Math.floor(a / b), a % b];
}
const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2
// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // ErrorEnums & Const Enums
Enums criam constantes nomeadas. String enums são recomendados (legíveis na saída, sem problemas de reverse mapping). Const enums são apagados em tempo de compilação (zero custo de runtime). Para casos simples, union types ('a' | 'b') são frequentemente melhores — sem código de runtime, melhor tree-shaking. Use enums para constantes agrupadas e documentadas.
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }
// String enum (recommended)
enum HttpStatus {
OK = "200 OK",
NotFound = "404 Not Found",
ServerError = "500 Internal Server Error",
}
// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;
// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";
// Exhaustive switch
function handle(s: Status) {
switch (s) {
case "pending": return "loading";
case "success": return "done";
case "error": return "failed";
}
}Dados Imutáveis
TypeScript oferece múltiplas ferramentas de imutabilidade: 'readonly' para propriedades, ReadonlyArray para arrays, utility Readonly<T> e 'as const' para deep readonly com tipos literal. Dados imutáveis previnem mutações acidentais e habilitam change detection (React, Redux). Use spread (...) para updates imutáveis — cria um novo objeto com campos modificados.
// Readonly modifier
interface User {
readonly id: number;
name: string;
}
// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error
// Readonly utility type
type FrozenUser = Readonly<User>;
// as const: deep readonly with literal types
const config = {
endpoint: "/api",
methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]
// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new objectMódulos & Namespaces
ES Modules: Import & Export
TypeScript usa sintaxe de ES module (import/export). Named exports são explícitos; default export é o 'main' export único. Use 'import * as' para namespace imports. Module resolution segue convenções do Node.js (node_modules, extensões). Configure 'module' e 'moduleResolution' no tsconfig.json.
// math.ts - exporting
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14159;
export default function multiply(a: number, b: number): number {
return a * b;
}
// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import
console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI); // 3.14159Type-Only Imports
'import type' importa apenas tipos (apagados em tempo de compilação, sem código de runtime). Isso evita dependências circulares e imports de runtime desnecessários. TS 4.5+ permite modificadores 'type' inline em imports mistos. Use type-only imports para interfaces, type aliases e enums (se const) para reduzir o bundle size.
// Type-only import (erased at runtime)
import type { User, Config } from "./types";
// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";
// Re-export types
export type { User } from "./types";
// Import type for interfaces and types
interface UserService {
get(id: string): User; // User from type-only import
}
// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoidDynamic Imports
Dynamic imports (import()) carregam módulos sob demanda, retornando uma Promise. Isso habilita code splitting e lazy loading — crítico para performance em web apps. TypeScript infere o tipo do módulo automaticamente. Use para recursos opcionais, bibliotecas grandes e code splitting baseado em rota (React.lazy, Next.js dynamic).
// Dynamic import returns a Promise
async function loadModule() {
const module = await import("./heavy-module");
module.doWork();
}
// Conditional loading
if (featureEnabled) {
const { Feature } = await import("./feature");
new Feature().init();
}
// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");
// With error handling
try {
const lib = await import("./optional-lib");
lib.run();
} catch (e) {
console.log("Library not available");
}Arquivos de Declaração & Module Augmentation
Arquivos de declaração (.d.ts) descrevem tipos para módulos JS, imports CSS/PNG e variáveis globais. Module augmentation estende tipos de módulos existentes — útil para adicionar propriedades a Express Request, Express Response ou tipos de third-party. É assim que middleware como passport adiciona tipagem req.user.
// global.d.ts - declare ambient modules
declare module "*.css" {
const content: string;
export default content;
}
declare module "*.png" {
const src: string;
export default src;
}
// Module augmentation - extend existing types
import express from "express";
declare module "express" {
interface Request {
user?: { id: string; name: string };
}
}
// Now req.user is typed
app.get("/", (req, res) => {
console.log(req.user?.name);
});Namespaces (Legado)
Namespaces são o sistema de módulos do TypeScript pré-ES6. Eles agrupam código relacionado sob um objeto nomeado. Para novos projetos, prefira ES modules (import/export) — são padronizados, tree-shakeable e funcionam com bundlers. Namespaces permanecem úteis em arquivos .d.ts para declarações de tipo globais e código legado.
// Namespace: pre-ES6 way to organize code
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true
// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarationsConfigurações de Módulo do tsconfig
Configurações de módulo do tsconfig controlam como TypeScript lida com imports. 'moduleResolution: node' usa resolução do Node.js (lookup de node_modules). 'esModuleInterop' habilita default imports de CommonJS. 'paths' cria import aliases (@/components) para imports mais limpos. 'resolveJsonModule' permite importar arquivos .json com tipos inferidos.
{
"compilerOptions": {
"module": "ESNext", // ES module output
"moduleResolution": "node", // Node-style resolution
"esModuleInterop": true, // allow default imports from CJS
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true, // import .json files
"isolatedModules": true, // each file is independent
"baseUrl": "./src", // base for non-relative imports
"paths": {
"@/*": ["./*"], // path alias
"@components/*": ["./components/*"]
}
}
}
// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"Async & Promises
Tipos Promise
Promise<T> é o tipo async core — T é o tipo do valor resolvido. TypeScript infere tipos através de cadeias .then(). Use 'new Promise()' para envolver APIs baseadas em callback. Sempre tipa os valores resolve/reject. Prefira async/await a cadeias .then() raw para legibilidade e tratamento de erros.
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) {
resolve({ id: 1, name: "Alice" });
} else {
reject(new Error("User not found"));
}
}, 100);
});
}
// Consuming a Promise
fetchUser(1)
.then(user => console.log(user.name))
.catch(err => console.error(err))
.finally(() => console.log("done"));
// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
// user is User, not any
console.log(user.name); // string
});async/await
async/await é açúcar sintático sobre Promises — 'await' pausa até a Promise resolver. Funções async sempre retornam uma Promise. Use Promise.all() para execução paralela (muito mais rápido que awaits sequenciais). Top-level await funciona em ES modules com ES2022+. TypeScript verifica que valores awaited são Promises.
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<User>;
}
// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);
// Sequential vs parallel
async function loadAll() {
// Sequential (slow)
const a = await getUser(1);
const b = await getUser(2);
// Parallel (fast)
const [x, y] = await Promise.all([getUser(1), getUser(2)]);
return { a, b, x, y };
}Tratamento de Erros em Async
Use try/catch com async/await para tratamento de erros — é mais limpo que .catch(). TypeScript não suporta throws tipados (todos os erros são unknown em catch), então estreite com instanceof. Para erros previsíveis, considere o padrão de tipo Result (union ok/error) em vez de exceções — torna o tratamento de erros explícito na assinatura de tipo.
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
try {
const data = await fetch("/api/data");
if (!data.ok) throw new Error(`HTTP ${data.status}`);
return await data.text();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
return "fallback";
} finally {
console.log("cleanup");
}
}
// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
// Result type as alternative to exceptions
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
async function safeFetch(url: string): Promise<Result<string>> {
try {
const res = await fetch(url);
return { ok: true, value: await res.text() };
} catch (e) {
return { ok: false, error: String(e) };
}
}Promise Combinators
Promise combinators orquestram múltiplas operações async: all() (paralelo, fail-fast), allSettled() (paralelo, espera todas), race() (primeira a settle), any() (primeira a ter sucesso). Use all() para carregamento de dados dependente, allSettled() quando quer resultados parciais, race() para timeouts, any() para fetches redundantes.
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
fetch("/api/a"),
fetch("/api/b"),
]);
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log(r.reason);
});
// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
fetch("/api/fast"),
fetch("/api/slow"),
]);
// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
fetch("/api/primary"),
fetch("/api/fallback"),
]);Event Loop & Microtasks
O event loop do JavaScript processa microtasks (callbacks de Promise, queueMicrotask) antes de macrotasks (setTimeout, setInterval). É por isso que Promises resolvem antes de timeouts. Async iteration (for await...of) consome async iterables — útil para streams. Async generators (async function*) produzem async iterables, habilitando sequências async preguiçosas.
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");
setTimeout(() => console.log("4: macrotask"), 0);
Promise.resolve().then(() => console.log("3: microtask"));
console.log("2: sync");
// Output: 1, 2, 3, 4
// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));
// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
for await (const chunk of stream) {
console.log(chunk.length);
}
}
// Async generators
async function* generate() {
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 100));
yield i;
}
}Padrões de Concorrência
Esses padrões são totalmente type-safe em TypeScript. Debounce atrasa execução até as chamadas pararem por N ms (input de busca). Throttle limita a uma chamada por N ms (handlers de scroll). Semaphore/mapLimit controla concorrência — útil para APIs rate-limited. Parameters<T> e ReturnType<T> preservam assinaturas de função em wrappers.
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Throttle
function throttle<T extends (...args: any[]) => void>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Semaphore (limit concurrency)
async function mapLimit<T, U>(
items: T[],
limit: number,
fn: (item: T) => Promise<U>
): Promise<U[]> {
const results: U[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const p = fn(item).then(r => results.push(r));
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(e => e === p), 1);
}
}
await Promise.all(executing);
return results;
}Tratamento de Erros & Testes
Try/Catch com Unknown
Desde TypeScript 4.4 (useUnknownInCatchVariables), erros capturados são 'unknown' — você deve estreitá-los antes do uso. Isso previne acessar propriedades que não existem. Use instanceof para verificar tipos de erro específicos, ou String() como fallback. Crie um helper getErrorMessage() para extração de erro consistente.
// In strict mode, catch is 'unknown' (not 'any')
try {
JSON.parse("invalid");
} catch (error: unknown) {
// Must narrow before using
if (error instanceof SyntaxError) {
console.error("JSON error:", error.message);
} else if (error instanceof Error) {
console.error(error.message);
} else {
console.error("Unknown error:", error);
}
}
// Helper function
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}Classes de Erro Personalizadas
Classes de erro personalizadas adicionam dados estruturados (code, statusCode, field) a erros. Sempre chame super(message) e defina o prototype (Object.setPrototypeOf) para corrigir o problema de prototype chain do TypeScript/ES5. Use instanceof para distinguir tipos de erro em blocos catch. Esse padrão é essencial para middleware Express e error handlers de API.
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = "AppError";
// Fix prototype chain (TS quirk)
Object.setPrototypeOf(this, AppError.prototype);
}
}
class ValidationError extends AppError {
constructor(message: string, public field: string) {
super(message, "VALIDATION_ERROR", 400);
this.name = "ValidationError";
}
}
// Usage
function createUser(input: unknown) {
if (typeof input !== "object" || input === null) {
throw new ValidationError("Invalid input", "body");
}
}
try {
createUser("bad");
} catch (e) {
if (e instanceof ValidationError) {
console.log(e.field, e.statusCode); // "body" 400
}
}Padrão de Tipo Result
O tipo Result (do Rust) torna erros explícitos na assinatura de tipo — chamadores devem tratar tanto sucesso quanto falha. Diferente de exceções, o compilador aplica tratamento de erros. Use isso para falhas esperadas (validação, not-found) onde exceções seriam exagero. Reserve exceções para erros verdadeiramente inesperados (bugs, falhas de sistema).
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { ok: false, error: "Division by zero" };
}
return { ok: true, value: a / b };
}
// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
console.log(result.value); // number
} else {
console.error(result.error); // string
}
// Utility helpers
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}Assertion Functions
Assertion functions (asserts X) lançam se uma condição falhar E estreitam o tipo depois. 'asserts value is string' diz ao TypeScript que após a chamada, value é string. Isso é mais limpo que if-checks repetidos. Use para validação de runtime em boundaries (input de API, config). Combine com Zod ou io-ts para validação de schema.
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string
// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
if (value === undefined) throw new Error("undefined");
}Parsing JSON Type-Safe
JSON.parse retorna 'any' — inseguro. Envolva-o com um type guard para validar a forma em runtime e estreitar o tipo. Para schemas complexos, use Zod, io-ts ou yup — eles geram tanto validators de runtime quanto tipos TypeScript a partir de uma única definição de schema. Isso é crítico para respostas de API e entrada do usuário.
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
try {
const parsed: unknown = JSON.parse(json);
if (guard(parsed)) return parsed;
return null;
} catch {
return null;
}
}
// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null
&& typeof (x as User).id === "number"
&& typeof (x as User).name === "string";
}
const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
console.log(data.name); // User
}
// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));Exhaustiveness Checking
Exhaustiveness checking garante que você trate todos os cases de um union. Atribua o case default a 'never' — se você adicionar uma nova variante ao union, TypeScript erros porque o novo tipo não é atribuível a 'never'. Isso captura cases ausentes em tempo de compilação. Essencial para discriminated unions, reducers do Redux e state machines.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; w: number; h: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.w * shape.h;
default:
// If a new shape is added, this errors
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'Decoradores & Metadata
Decoradores de Classe
Decoradores de classe recebem a função construtora e podem retornar uma classe modificada. Decorator factories (retornando uma função) aceitam argumentos. Decoradores são um recurso experimental — habilite 'experimentalDecorators' no tsconfig. Fortemente usados em NestJS, TypeORM e Angular para dependency injection e metadata.
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
return class extends target {
constructor(...args: any[]) {
super(...args);
console.log(`Created ${target.name}`);
}
};
}
@Logged
class Service {
constructor(public name: string) {}
}
const s = new Service("Auth");
// Logs: "Created Service"
// Decorator factory (with arguments)
function Prefix(prefix: string) {
return function <T extends new (...args: any[]) => any>(target: T): T {
return class extends target {
message = prefix + " " + (this as any).name;
};
};
}Decoradores de Método & Propriedade
Decoradores de método recebem (target, propertyKey, descriptor) e podem envolver o método original — útil para logging, caching e controle de acesso. Decoradores de propriedade recebem (target, key) e são frequentemente usados para registrar metadata. O descriptor.value é a função original; envolva-o para adicionar comportamento. Comum em NestJS (@Get, @Post) e TypeORM (@Column).
// Method decorator: (target, key, descriptor)
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}(${args})`);
return original.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"
// Property decorator: (target, key)
function Required(target: any, key: string) {
// Store metadata for validation
console.log(`Required: ${key}`);
}Decoradores de Parâmetro & Metadata
Decoradores de parâmetro recebem (target, key, index) e são usados para dependency injection (NestJS, Angular). O polyfill 'reflect-metadata' habilita metadata de tipo em runtime — decoradores podem acessar tipos de parâmetro via Reflect.getMetadata('design:paramtypes'). É assim que DI containers sabem o que injetar. Habilite com 'emitDecoratorMetadata: true' no tsconfig.
import "reflect-metadata";
// Parameter decorator
function Inject(target: any, key: string, index: number) {
console.log(`Inject param ${index} of ${key}`);
}
class Service {
constructor(@Inject private dep: any) {}
}
// Store and retrieve metadata
const METADATA_KEY = "design:type";
class Example {
greet(name: string): void {}
}
// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)
const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]Accessor Decorators
Accessor decorators aplicam-se a getters/setters. O descriptor tem propriedades get/set que você pode envolver. Use-os para validação, logging ou mudar enumerability. O padrão de validação (MaxLength, Min, Max) envolve o setter para aplicar constraints em runtime. É assim que class-validator (NestJS) funciona para validação de DTO.
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
return function (
target: any,
key: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Person {
private _name: string = "";
@Enumerable(true)
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)
// Validation decorator
function MaxLength(len: number) {
return function (target: any, key: string) {
let value: string;
Object.defineProperty(target, key, {
get() { return value; },
set(v: string) {
if (v.length > len) throw new Error("Too long");
value = v;
},
});
};
}Decoradores Modernos (TC39 Stage 3)
TypeScript 5.0 suporta a proposta de decorator TC39 Stage 3 — uma API padronizada substituindo experimentalDecorators. A nova API usa um objeto de contexto (ClassMethodDecoratorContext) em vez de (target, key, descriptor). É mais limpa, type-safe e eventualmente estará no padrão JS. Use isso para novos projetos; decoradores experimentais permanecem para compatibilidade com NestJS/Angular.
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object
function log<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
return function (this: This, ...args: Args): Return {
console.log(`Calling ${String(context.name)}`);
return target.call(this, ...args);
};
}
class Service {
@log
greet(name: string): string {
return `Hello, ${name}`;
}
}
// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after constructionDecorador Prático: Memoize
Esse decorador memoize armazena em cache resultados de método com base em argumentos — speedups dramáticos para funções puras caras como fibonacci. O cache é por-instância (use um WeakMap para cache compartilhado). Decoradores brilham para cross-cutting concerns: logging, caching, validação, controle de acesso, lógica de retry. Eles mantêm a lógica de negócios limpa separando concerns de infraestrutura.
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
const cache = new Map<string, Return>();
return function (this: This, ...args: Args): Return {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = target.call(this, ...args);
cache.set(key, result);
return result;
};
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n < 2) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");
console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");Utility Types
Partial, Required & Readonly
Partial<T> torna todas as propriedades opcionais — perfeito para operações de update/patch onde apenas alguns campos mudam. Required<T> é o inverso. Readonly<T> torna todas as propriedades imutáveis em tempo de compilação. Esses são os utility types mais comumente usados e eliminam a necessidade de manter interfaces optional/readonly paralelas manualmente.
interface User {
id: number;
name: string;
email: string;
}
// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }
function updateUser(id: number, changes: UserUpdate) {
// Only provided fields are updated
Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name
// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required
// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly
// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutationPick, Omit & Record
Pick<T, K> extrai um subconjunto de propriedades; Omit<T, K> remove propriedades — ambos criam tipos derivados sem duplicação. Record<K, V> cria um tipo map/dicionário com chaves específicas. Esses são essenciais para DTOs (data transfer objects): derive um tipo CreateUser de User omitindo campos auto-gerados como id e createdAt. Isso mantém os tipos DRY e sincronizados.
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: Date;
}
// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }
// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;ReturnType, Parameters & Awaited
ReturnType e Parameters extraem tipos de funções existentes — inestimável ao envolver ou chamar funções cujas assinaturas você não quer duplicar. Awaited<T> desembrulha Promises aninhadas (Promise<Promise<T>> torna-se T), essencial para tipos de retorno de função async. InstanceType obtém o tipo de instância de um construtor de classe. Esses habilitam composição de função type-safe e utilidades de ordem superior.
function fetchUser(id: number): Promise<{ name: string; age: number }> {
return Promise.resolve({ name: "Alice", age: 30 });
}
// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>
// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }
// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]
// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number
// ConstructorParameters<T>: parameters of a class constructor
class Point {
constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]
// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // PointExclude, Extract & NonNullable
Exclude<T, U> remove tipos de um union; Extract<T, U> mantém apenas tipos correspondentes — ambos operam em membros de union. NonNullable<T> remove null e undefined. Esses são building blocks: Omit é definido como Pick<T, Exclude<keyof T, K>>. Use Exclude/Extract para filtrar tipos de union dinamicamente, ex.: separar tipos de erro de tipos de sucesso em um union de resultado.
type Role = "admin" | "user" | "guest" | "superadmin";
// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"
// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"
// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User
// Practical: filter union types
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"
// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;Custom Utility Types
Custom utility types compõem built-ins para necessidades específicas. Optional<T, K> torna apenas certos campos opcionais (mais direcionado que Partial). DeepPartial/DeepReadonly aplicam-se recursivamente a objetos aninhados — útil para config e árvores de estado. O modificador -readonly em Mutable remove readonly. Esses padrões mostram como mapped types e conditional types se combinam para programação em nível de tipo poderosa.
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }
// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Mutable (remove readonly from all properties)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
[K in keyof T]: T[K] extends never ? never : K;
}[keyof T];Conditional Types
Conditional Types Básicos (T extends U ? X : Y)
Conditional types (T extends U ? X : Y) selecionam um tipo com base em uma condição em nível de tipo — como um ternário para tipos. Eles são a base da programação em nível de tipo do TypeScript. Quando T é um union, a condição distribui sobre cada membro (distributive conditional types). A keyword infer extrai tipos de dentro de um padrão, como puxar o tipo de elemento de um array ou o tipo de resolução de uma Promise.
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y
// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
type C = ElementOf<string>; // never (not an array)
// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type D = UnwrapPromise<Promise<number>>; // number
type E = UnwrapPromise<string>; // string (not a promise)
// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type F = IsFunction<() => void>; // true
type G = IsFunction<number>; // false
// Conditional types are evaluated lazily and distribute over unionsKeyword infer (Extração de Tipo)
A keyword infer declara uma variável de tipo dentro da cláusula extends de um conditional type, capturando qualquer tipo que corresponda a essa posição. É como ReturnType, Parameters e Awaited são implementados. infer pode ser usado recursivamente (Unwrap<Promise<Promise<T>>>) para desembrulhar totalmente tipos aninhados. É a ferramenta principal para extrair tipos de estruturas genéricas complexas.
// infer extracts a type from within a pattern
// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string
// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)
// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number
// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;
// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;Distributive Conditional Types
Conditional types distribuem sobre unions: aplicar ToArray<A | B> dá ToArray<A> | ToArray<B>, não (A | B)[]. É assim que Exclude e NonNullable filtram membros de union — eles retornam 'never' para tipos excluídos, que colapsa no union. Para prevenir distribuição, envolva ambos os lados em colchetes: [T] extends [U]. Distribuição é geralmente o que você quer para filtragem, mas non-distributive é necessário para operações 'envolva todo o union'.
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y applied to A | B becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]
// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[] — a single array of the union
// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number (null and undefined filtered out)
// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;Conditional Type Constraints
Conditional types podem ser aninhados para criar discriminação em nível de tipo (como um switch statement para tipos). Combinados com infer, eles extraem e derivam tipos de parâmetros genéricos. É assim que bibliotecas como React derivam tipos de prop a partir de definições de componente, e como bibliotecas de routing extraem tipos de parâmetro de strings de caminho. A constraint (T extends any[]) garante que a entrada é válida antes de o conditional extrair o tipo de elemento.
// Use conditional types to constrain and derive types
// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type
// Conditional with multiple branches (nested)
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>; // "object"
// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // numberTemplate Literal Types (Manipulação de Strings)
Template literal types habilitam manipulação de strings em nível de tipo — concatenação, conversão de caso e pattern matching. Combinados com conditional types e infer, eles podem fazer parse de strings de caminho para extrair parâmetros de rota, gerar nomes de event handler ou construir accessors de propriedade type-safe. É assim que frameworks como Next.js e tRPC criam APIs type-safe end-to-end a partir de string literals.
// Template literal types: type-level string operations
type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">; // "Foo"
// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Extract route parameters
type ExtractParams<Path extends string> =
Path extends `${infer _Start}/${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${infer _Start}/${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"
// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T ? Get<T[Key], Rest> : never
: P extends keyof T ? T[P] : never;Mapped Types
Mapped Types Básicos
Mapped types iteram sobre as chaves de um objeto e transformam cada propriedade — [K in keyof T] é a sintaxe. É como Partial, Readonly, Pick e outros utility types são implementados. Você pode modificar o tipo de propriedade (T[K] | null), adicionar modificadores (? ou readonly) ou substituir completamente o tipo de valor. Mapped types são a espinha dorsal do sistema de transformação de tipos do TypeScript.
// Mapped types transform each property of an existing type
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
id: number;
name: string;
email: string;
}
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }
// Change all property types to a specific type
type Stringify<T> = {
[K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }Key Remapping via 'as'
Key remapping (cláusula as, TS 4.1+) permite renomear ou filtrar chaves durante mapping. Use template literal types para transformar nomes de chaves (adicionar prefixos, converter para getters, uppercase). Retornar 'never' para uma chave a remove — é assim que você filtra propriedades. Combinado com conditional types, key remapping habilita transformações poderosas como converter um schema de dados para um schema de validação ou um tipo de API para um tipo de formulário.
// Remap keys using 'as' (TypeScript 4.1+)
// Add a prefix to all keys
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface User {
id: number;
name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }
// Convert keys to getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }
// Filter out keys by returning never
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }
// Make keys UPPERCASE
type UpperKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};Modificadores: +, -, ?, readonly
Os modificadores + e - adicionam ou removem modificadores de propriedade. -? remove opcionalidade (tornando fields opcionais obrigatórios); -readonly remove imutabilidade. É assim que Required<T> e o padrão Mutable funcionam. O prefixo + é opcional (readonly é o mesmo que +readonly), mas - é necessário para remoção. Isso dá controle fino sobre características de propriedade durante transformações de tipo.
// Add (+) or remove (-) modifiers
// Remove optional (?) modifier: -?
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
interface OptionalUser {
id?: number;
name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now
// Remove readonly modifier: -readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface FrozenConfig {
readonly port: number;
readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now
// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
+readonly [K in keyof T]: T[K];
};
// Add optional modifier: +?
type MakeOptional<T> = {
[K in keyof T]+?: T[K];
};Homomorphic Mapped Types
Homomorphic mapped types ([K in keyof T]) preservam modificadores de propriedade (readonly, ?) do tipo de origem — é por isso que Pick<User, 'id'> mantém id readonly. Mappings non-homomorphic (ex.: [K in string]) não preservam modificadores. Isso importa ao derivar tipos: um Partial homomorphic de um tipo com fields readonly mantém esses fields readonly (mas opcionais). Entender homomorphism ajuda a prever se modificadores sobrevivem a uma transformação.
// Homomorphic mapped types preserve modifiers from the original type
interface User {
readonly id: number;
name: string;
email?: string;
}
// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
[K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User
// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
[K in string]: T; // not keyof T — loses modifiers
};
// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!
// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
[K in keyof T]?: T[K]; // Partial is homomorphic
};Construindo um Tipo de Validação a partir de um Schema
É assim que bibliotecas de formulário (React Hook Form, Formik) e bibliotecas de validação (Zod, Yup) mantêm type safety — elas derivam tipos de validator e form a partir de suas interfaces de dados usando mapped types. Quando você adiciona um field a User, os tipos de validator e form automaticamente o exigem também, prevenindo drift. Isso demonstra o poder de mapped types no mundo real: uma fonte de verdade (a interface) impulsiona múltiplos tipos derivados.
// Practical: derive a validator type from a data type
// Given a data interface
interface User {
id: number;
name: string;
email: string;
}
// Create a validator type where each field is a validation function
type Validator<T> = {
[K in keyof T]: (value: T[K]) => boolean;
};
const userValidator: Validator<User> = {
id: (v) => v > 0,
name: (v) => v.length > 0,
email: (v) => v.includes("@"),
};
// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
value: T;
error: string | null;
touched: boolean;
};
type Form<T> = {
[K in keyof T]: FormField<T[K]>;
};
const userForm: Form<User> = {
id: { value: 1, error: null, touched: false },
name: { value: "Alice", error: null, touched: true },
email: { value: "", error: "Required", touched: true },
};
// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.Type Guards & Narrowing
Narrowing com typeof & instanceof
TypeScript estreita tipos com base em verificações de runtime. typeof estreita primitives (string, number, boolean, etc.); instanceof estreita instâncias de classe. Truthiness checks (if (value)) estreitam para fora null/undefined/0/''/false. O narrowing aplica-se dentro do branch onde a condição vale. É assim que TypeScript faz verificações de runtime carregarem informação de tipo, eliminando a necessidade de casts explícitos.
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
// padding is narrowed to 'number' here
}
return padding + value;
// padding is narrowed to 'string' here
}
// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }
function speak(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow(); // OK — narrowed to Cat
} else {
animal.bark(); // OK — narrowed to Dog
}
}
// typeof returns: "string" | "number" | "boolean" | "symbol"
// "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)
// Truthiness narrowing
function process(value?: string) {
if (value) {
console.log(value.toUpperCase()); // value is string (not undefined)
}
}Operador in & Discriminated Unions
O operador 'in' estreita com base em existência de propriedade. Discriminated unions usam uma propriedade literal compartilhada (como 'kind' ou 'type') como tag — switch nela estreita para a variante correta com acesso total a propriedades. Esse é o equivalente TypeScript de sum types / algebraic data types. É o padrão padrão para actions do Redux, state machines e respostas de API com múltiplas formas. Sempre use um tipo literal para o discriminant.
// 'in' operator checks for a property — narrows to types that have it
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // narrowed to Fish
} else {
animal.fly(); // narrowed to Bird
}
}
// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // narrowed by kind
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)Funções Custom Type Guard
Custom type guards (tipo de retorno 'x is Type') permitem encapsular verificações de runtime complexas em funções reutilizáveis que estreitam tipos. Assertion functions (asserts x is T) lançam em vez de retornar boolean — elas estreitam em todo código após a chamada. Use type guards para validar dados não confiáveis (JSON.parse, respostas de API) e trazê-los para o type system. Isso faz a ponte entre validação de runtime e tipos em tempo de compilação.
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true
interface User {
id: number;
name: string;
}
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.id === "number" &&
typeof obj.name === "string"
);
}
const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');
if (isUser(data)) {
console.log(data.name); // data is narrowed to User
}
// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}
// Type guard for discriminated unions
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
return r.success;
}
// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("Expected value to be defined");
}
}Exhaustiveness Checking com never
Exhaustiveness checking usa o tipo 'never' para garantir que todas as variantes de union sejam tratadas. Se você adicionar uma nova variante ao union, mas esquecer um case, a atribuição 'never' do branch default torna-se um erro de compilação. O helper assertNever lança em runtime e erros em tempo de compilação para cases ausentes. Esse é o padrão mais valioso para discriminated unions — faz o compilador dizer quando você esqueceu um case.
// The 'never' type ensures all cases are handled
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
// If we add a new shape but forget a case, this line
// becomes a compile error because 'shape' would not be 'never'
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'
// Helper function pattern
function assertNever(x: never): never {
throw new Error("Unexpected: " + JSON.stringify(x));
}
function process(shape: Shape) {
switch (shape.kind) {
case "circle": return;
case "square": return;
default: return assertNever(shape); // compile error if a case is missing
}
}Narrowing com Métodos de Array
Array.filter não estreita tipos de elemento por padrão porque seu callback retorna boolean, não um type guard. Para estreitar, passe uma função custom type guard (pet is Dog) — então filter retorna o tipo de array estreitado. TypeScript também estreita dentro de corpos de callback (forEach, map) com base em if-checks. Array.isArray é um type guard integrado que estreita unknown/any para um tipo de array.
// TypeScript narrows through filter, map, and control flow
interface Pet {
name: string;
speak(): void;
}
class Dog implements Pet {
name = "Rex";
speak() { console.log("Woof"); }
fetch() { console.log("Fetching!"); }
}
class Cat implements Pet {
name = "Whiskers";
speak() { console.log("Meow"); }
}
const pets: Pet[] = [new Dog(), new Cat(), new Dog()];
// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
return pet instanceof Dog;
}
const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog
// Narrowing in forEach/callbacks
pets.forEach((pet) => {
if (isDog(pet)) {
pet.fetch(); // narrowed to Dog inside callback
}
});
// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
if (Array.isArray(input)) {
input.length; // OK — input is any[] (or unknown[])
}
}Type Inference
Inferência de Variável & Tipo de Retorno
TypeScript infere tipos a partir de initializers e return statements, então você raramente precisa de anotações explícitas. Variáveis widen para seu tipo geral (let x = 10 infere number, não 10). 'as const' previne widening: faz literals permanecerem literal, objetos readonly e arrays se tornarem readonly tuples. typeof colors[number] extrai um union de tipos de elemento de tuple — um padrão comum para derivar tipos tipo-enum a partir de arrays.
// TypeScript infers types when you don't annotate
// Variable inference
let count = 0; // number
let name = "Alice"; // string
let items = [1, 2, 3]; // number[]
let mixed = [1, "two"]; // (string | number)[]
// Function return type inference
function add(a: number, b: number) {
return a + b; // return type inferred as number
}
// const assertions for literal types
const x = 10; // number (widened)
const y = "hello"; // string (widened)
const z = { a: 1 }; // { a: number }
const x2 = 10 as const; // 10 (literal type)
const y2 = "hello" as const; // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }
// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"Contextual Typing
Contextual typing flui o tipo esperado backward em expressões. Quando você atribui uma função a uma variável tipada, os tipos de parâmetro são inferidos a partir do tipo alvo. É por isso que event handlers, callbacks de array e object literals frequentemente não precisam de anotações de tipo. A regra geral: anote assinaturas de função (parâmetros e tipos de retorno para APIs públicas), mas deixe a inferência lidar com locals e callbacks.
// Contextual typing: the expected type influences the inferred type
// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
// mouseEvent is inferred as MouseEvent (from onmousedown's type)
console.log(mouseEvent.button);
};
// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]
// Contextual typing for object literals
interface User {
name: string;
age: number;
}
const user: User = {
name: "Alice", // inferred as string (from User.name)
age: 30,
};
// Contextual typing with discriminated unions
type Event =
| { type: "click"; x: number; y: number }
| { type: "scroll"; scrollTop: number };
function handle(e: Event) {
if (e.type === "click") {
console.log(e.x, e.y); // narrowed
}
}
// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation neededBest Common Type (Inferência de Union)
Ao inferir a partir de múltiplos valores (como array literals), TypeScript encontra o 'best common type' — geralmente um supertipo ou um union. Um array de [Dog, Cat] infere como Animal[] (a base comum), não (Dog | Cat)[]. Para obter um union, anote explicitamente. Conditional returns inferem o union de todos os branches. Entender isso ajuda a prever quando você precisa de anotações explícitas vs quando a inferência basta.
// When inferring from multiple values, TS finds the best common type
// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]
// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]
// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }
const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])
// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];
// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]
// Return type inference with conditionals
function getValue(flag: boolean) {
return flag ? 42 : "hello"; // inferred as number | string
}Control Flow Analysis
TypeScript realiza control flow analysis — rastreia como tipos estreitam e widen através de if/else, returns, assignments e logical operators. Um tipo estreita após uma verificação e permanece estreitado até a variável ser reatribuída. Early returns (guard clauses) são especialmente eficazes: após 'if (value === null) return', o resto da função sabe que value não é null. É por isso que código estilo guard-clause funciona tão bem com TypeScript.
// TypeScript tracks types through control flow
function example(value: string | number | null) {
// After this check, 'value' is string | number in this branch
if (value === null) {
return;
}
// value is now string | number (null narrowed out)
console.log(value);
if (typeof value === "string") {
return value.toUpperCase(); // value is string
}
// value is now number (string and null narrowed out)
return value.toFixed(2);
}
// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number
// Narrowing with && and ||
function process(input?: string) {
const value = input && input.trim(); // string | undefined
const safe = input || "default"; // string (undefined narrowed out)
}
// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCaseOperador satisfies (TS 4.9+)
O operador 'satisfies' (TS 4.9+) valida que um valor conforma a um tipo enquanto preserva o tipo inferido mais específico — diferente de anotações de tipo que widen. Isso é ideal para configs, route maps e objetos de theme: você obtém validação em tempo de compilação de que a estrutura está correta, mas acesso a propriedade ainda retorna o tipo literal preciso. Combine com 'as const' para tanto preservação literal quanto validação estrutural.
// 'satisfies' checks a value matches a type WITHOUT widening it
// Problem: annotation widens the type
const config1: Record<string, string | number> = {
port: 8080,
host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)
// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
port: 8080,
host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated
// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;
const routes = {
"/users": { method: "GET", handler: () => fetchUsers() },
"/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;
routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)
// 'as const' + 'satisfies': literal types + validation
const colors = {
primary: "#ff0000",
secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;Arquivos de Declaração & Module Augmentation
Escrevendo Arquivos de Declaração .d.ts
Arquivos .d.ts contêm declarações de tipo (sem implementação) — eles descrevem os tipos de código JavaScript. Use 'declare module' para adicionar tipos a pacotes npm não tipados. 'declare global' estende tipos globais como Window. Ambient declarations dizem ao TypeScript 'isso existe em runtime, confie em mim'. É assim que você integra JS legado, APIs de navegador e variáveis injetadas em build-time ao type system.
// types/my-module.d.ts — describes the types of a JS library
// Declare a module (for libraries without types)
declare module "untyped-lib" {
export function greet(name: string): string;
export const version: string;
export interface Config {
timeout: number;
retries: number;
}
}
// Ambient declarations for global variables
declare global {
interface Window {
myCustomProp: string;
myApp: { init: () => void };
}
}
// Now window.myCustomProp is typed
// window.myCustomProp; // string
// Declaring a global function
declare function myGlobalFn(x: number): string;
// Declaring a global namespace
declare namespace MyLib {
function doSomething(): void;
const version: string;
}
// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constantsModule Augmentation (Estendendo Tipos Existentes)
Module augmentation estende tipos existentes de outros módulos — adicionando propriedades a interfaces sem modificar a fonte original. É assim que middleware Express (como passport) adiciona req.user, e como você estende tipos de bibliotecas de third-party. A sintaxe 'declare module' reabre o espaço de tipo do módulo. Augmentations devem estar em um módulo (um arquivo com import/export) para ter efeito global.
// Module augmentation: add to an existing module's types
// Extend an interface from another module
import express from "express";
declare module "express" {
interface Request {
user?: {
id: number;
name: string;
};
// Now req.user is typed on all Express requests
}
}
// Augment a third-party module
declare module "axios" {
export interface AxiosRequestConfig {
retryCount?: number; // add a custom config option
}
}
// Augment a global interface
declare global {
interface Array<T> {
last(): T | undefined; // add a custom array method
}
}
// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3 — now typed!
// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.Triple-Slash Directives
Triple-slash directives (///) são comentários especiais do compilador que instruem TypeScript a incluir arquivos ou pacotes de tipo adicionais. O mais comum é /// <reference types='node' /> para incluir @types/node. Com as opções 'types' e 'lib' modernas do tsconfig.json, essas raramente são necessárias — prefira configurações baseadas em config. Elas são vistas principalmente em arquivos .d.ts e código legado. Entendê-las ajuda ao ler declaration files.
// Triple-slash directives are special comments processed by TS
/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)
/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)
/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")
// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />
declare function readFile(path: string): Buffer; // Buffer from @types/node
// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.
// Prefer tsconfig.json settings:
// {
// "compilerOptions": {
// "types": ["node"], // instead of /// <reference types="node" />
// "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
// }
// }Publicando Tipos com um Pacote
Para publicar tipos TypeScript com seu pacote npm, defina 'types' em package.json para apontar para seu arquivo .d.ts e habilite 'declaration: true' no tsconfig. Consumidores obtêm tipos automaticamente quando instalam seu pacote. declarationMap habilita 'Go to Definition' para pular para o arquivo fonte .ts. Para bibliotecas sem tipos empacotados, o projeto DefinitelyTyped (@types/package) fornece declarações mantidas pela comunidade.
// package.json for a library with TypeScript types
{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts", // <-- points to the type declarations
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
// tsconfig.json for building a library
{
"compilerOptions": {
"declaration": true, // generate .d.ts files
"declarationMap": true, // generate .d.ts.map (source maps for types)
"sourceMap": true, // generate .js.map
"outDir": "./dist",
"rootDir": "./src",
"composite": true // enable project references
}
}
// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!
// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express
// Check if types exist: https://www.typescriptlang.org/dt/searchType-Only Imports & Exports
'import type' importa apenas informação de tipo — é completamente apagado em runtime, reduzindo o bundle size e evitando problemas de dependência circular. Use-o para interfaces, type aliases e re-exports type-only. A sintaxe inline 'import { x, type Y }' (TS 4.5+) mistura imports de valor e tipo de forma limpa. verbatimModuleSyntax (TS 5.0+) aplica isso estritamente. Prefira 'import type' sempre que importar algo usado apenas em posições de tipo.
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";
// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies
// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only
// 'export type' re-exports types only
export type { User, Config } from "./types";
// Interface vs Type for declarations
interface IUser { name: string; } // can be augmented/merged
type TUser = { name: string }; // cannot be merged, more flexible
// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
// "compilerOptions": { "verbatimModuleSyntax": true }
// }Opções do tsconfig.json
Opções Core do Compilador
O tsconfig.json controla como TypeScript compila. 'target' define a versão JS de saída; 'module' define o sistema de módulos. 'strict: true' é a configuração mais importante — habilita todas as verificações de tipo strict (noImplicitAny, strictNullChecks, etc.). 'lib' determina quais APIs integradas estão disponíveis (DOM para navegador, ES2022 para recursos JS modernos). Sempre comece novos projetos com strict: true.
{
"compilerOptions": {
"target": "ES2022", // JS version to compile to
"module": "ESNext", // module system (ESNext, CommonJS, etc.)
"moduleResolution": "bundler", // how modules are resolved
"lib": ["ES2022", "DOM"], // available type definitions
"outDir": "./dist", // output directory for compiled JS
"rootDir": "./src", // root of source files
"sourceMap": true, // generate .map files for debugging
"declaration": true, // generate .d.ts files (for libraries)
"removeComments": true, // strip comments from output
// Type checking strictness
"strict": true, // enable ALL strict checks (recommended)
"noImplicitAny": true, // error on implicit 'any'
"strictNullChecks": true, // null/undefined are separate types
"noUnusedLocals": true, // error on unused local variables
"noUnusedParameters": true,// error on unused function params
"noImplicitReturns": true, // error if not all paths return
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true, // allow default imports from CommonJS
"skipLibCheck": true, // skip type checking of .d.ts files
"forceConsistentCasingInFileNames": true
}
}Flags do Strict Mode Explicadas
Strict mode é um pacote de flags de rigor. strictNullChecks é o mais impactante — torna null/undefined tipos distintos, forçando você a tratá-los explicitamente (a fonte #1 de crashes de runtime). noImplicitAny previne erosão silenciosa de tipo. strictPropertyInitialization captura fields de classe não inicializados (use ! para definite assignment ou inicialize no construtor). Sempre habilite strict mode em novos projetos — o custo inicial vale a segurança.
{
"compilerOptions": {
// 'strict: true' enables ALL of these:
"strictNullChecks": true,
// null and undefined are NOT assignable to other types
// without explicit union. Forces null handling.
let x: string = null; // ERROR (without this, it's allowed)
"noImplicitAny": true,
// Parameters/variables can't be implicitly 'any'
function fn(x) { } // ERROR: x is implicitly any
"strictFunctionTypes": true,
// Function parameter types checked contravariantly
// (catches unsafe function assignments)
"strictBindCallApply": true,
// bind/call/apply are type-checked
"strictPropertyInitialization": true,
// Class properties must be initialized or declared with !
class User {
name: string; // ERROR: not initialized
name2!: string; // OK: definite assignment assertion
}
"noImplicitThis": true,
// 'this' must have a known type (no implicit any)
"alwaysStrict": true
// Emit "use strict" in every file
}
}Estratégias de Module Resolution
moduleResolution controla como caminhos de import são resolvidos. 'node' é a estratégia clássica; 'bundler' (TS 5.0+) corresponde a bundlers modernos como Vite e suporta exports do package.json. 'nodenext' é ESM estrito (requer extensões). paths permite criar import aliases (@/ → src/), que devem ser espelhados na config do seu bundler (ex.: resolve.alias do Vite). baseUrl + paths é a forma padrão de evitar imports relativos profundos (../../../).
{
"compilerOptions": {
// How TS resolves import paths
"moduleResolution": "node", // classic Node.js resolution
// Looks for: file.ts, file/index.ts, node_modules/file
"moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
// Matches how bundlers resolve: supports import maps,
// conditional exports, no file extension requirement
"moduleResolution": "nodenext", // Node.js ESM resolution (strict)
// Requires file extensions in imports: import "./foo.js"
// Path mapping (aliases)
"baseUrl": ".",
"paths": {
"@/*": ["src/*"], // import "@/components/Button"
"@utils/*": ["src/utils/*"],
"@components": ["src/components/index.ts"]
}
// rootDirs: virtual directories that map to the same location
"rootDirs": ["src", "generated"],
// imports between src/ and generated/ resolve as if same dir
}
}
// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.tsProject References (Monorepos)
Project references dividem uma grande codebase em sub-projetos compilados independentemente — essencial para monorepos. Cada projeto tem composite: true e emite declarações. References declara dependências entre projetos. tsc --build (-b) compila em ordem de dependência, apenas recompilando o que mudou. Isso acelera dramaticamente a verificação de tipos para grandes codebases e impõe boundaries arquiteturais entre pacotes.
// tsconfig.json (root — references sub-projects)
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true, // required for project references
"declaration": true, // must emit declarations
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// packages/api/tsconfig.json
{
"compilerOptions": { /* ... */ },
"references": [
{ "path": "../shared" } // depends on shared
]
}
// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically
// Build command: tsc --build (or tsc -b)Receitas Comuns do tsconfig
Diferentes tipos de projeto precisam de configurações tsconfig diferentes. React/Vite usa jsx: 'react-jsx' e noEmit (Vite compila). Node.js usa CommonJS (ou NodeNext para ESM) e types: ['node']. Bibliotecas precisam declaration: true para saída .d.ts e um target mais baixo para compatibilidade mais ampla. isolatedModules é exigido pelo Vite/esbuild (cada arquivo deve ser compilável independentemente). Sempre exclua arquivos de teste e node_modules do build.
// React + Vite project
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx", // React 17+ JSX transform
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noEmit": true, // Vite handles compilation
"isolatedModules": true, // required by Vite/esbuild
"verbatimModuleSyntax": true
},
"include": ["src"]
}
// Node.js backend project
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS", // or NodeNext for ESM
"moduleResolution": "node",
"lib": ["ES2022"],
"types": ["node"], // @types/node
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
// Library project (publishes to npm)
{
"compilerOptions": {
"target": "ES2020", // broader compatibility
"module": "ESNext",
"declaration": true, // emit .d.ts
"declarationMap": true,
"outDir": "./dist",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}Decoradores
Decorador de Classe
Decoradores de classe recebem o construtor e podem retornar uma classe modificada. Eles são experimentais (requerem experimentalDecorators: true). Comuns em NestJS e TypeORM.
function logged<T extends new (...args: any[]) => any>(con: T) {
return class extends con {
created = new Date().toISOString();
};
}
@logged
class Service { constructor(public name: string) {} }Decorador de Método
Decoradores de método recebem (target, key, descriptor). Envolver descriptor.value habilita logging, caching, validação. É assim que interceptors do NestJS funcionam.
function log(target: any, key: string, desc: PropertyDescriptor) {
const orig = desc.value;
desc.value = function(...args: any[]) {
console.log(`Calling ${key}`, args);
return orig.apply(this, args);
};
}
class Calc { @log add(a: number, b: number) { return a + b; } }Decorador de Propriedade
Decoradores de propriedade recebem (target, key). Usar Object.defineProperty cria getters/setters para validação. Usado em class-validator para validação de DTO.
function required(target: any, key: string) {
let val: any;
Object.defineProperty(target, key, {
get() { return val; },
set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
});
}Decorador de Parâmetro
Decoradores de parâmetro recebem (target, methodKey, parameterIndex). Usados com metadata reflection para validação. class-validator e NestJS usam isso.
function Min(min: number) {
return (target: any, key: string, idx: number) => {
console.log(`${key} param ${idx} >= ${min}`);
};
}
class Order { create(@Min(0) qty: number) { return qty; } }Decorator Factory
Decorator factories retornam uma função decoradora, habilitando configuração. A função externa recebe parâmetros, a interna é o decorador real.
function Retry(times: number) {
return function(target: any, key: string, desc: PropertyDescriptor) {
const orig = desc.value;
desc.value = async function(...args: any[]) {
for (let i = 0; i < times; i++)
try { return await orig.apply(this, args); }
catch (e) { if (i === times - 1) throw e; }
};
};
}Module Augmentation
Aumentar Tipos Built-in
Module augmentation estende tipos existentes. declare global permite aumentar tipos built-in como Array. A implementação em runtime também deve ser fornecida.
declare global {
interface Array<T> {
last(): T | undefined;
chunk(size: number): T[][];
}
}
Array.prototype.last = function() { return this[this.length - 1]; };Aumentar Tipos de Biblioteca
Module augmentation estende tipos de bibliotecas de third-party. declare module reabre o espaço de tipo do módulo. Essencial para adicionar propriedades personalizadas a objetos de framework.
declare module 'express' {
interface Request {
user?: { id: string; role: string };
}
}
app.get('/profile', (req, res) => {
const userId = req.user?.id; // Typed!
});Aumentar Window
Aumentar Window adiciona propriedades globais personalizadas com type safety. Útil para expor estado do app a ferramentas de depuração ou analytics.
declare global {
interface Window {
myApp: { init: () => void; version: string };
}
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };CSS Modules
CSS Modules precisam de declarações de tipo. A declaração mapeia imports .module.css para um record de nomes de classe. Habilita autocompletar para referências de classe CSS.
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />Vue Plugin
Vue e outros frameworks usam module augmentation para tipagem de plugin. ComponentCustomProperties adiciona propriedades de instância. Habilita plugins type-safe.
declare module 'vue' {
interface ComponentCustomProperties {
$auth: { login: () => Promise<void> };
}
}
export default defineComponent({
methods: { async login() { await this.$auth.login(); } }
});Declaration Merging
Mesclando Interfaces
Interfaces com o mesmo nome são automaticamente mescladas. Todos os membros tornam-se parte de uma única interface. Útil para dividir interfaces entre arquivos.
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };Mesclando Namespaces
Namespaces com o mesmo nome mesclam seus exports. Isso permite dividir conteúdo de namespace entre arquivos. ES modules são preferidos para código novo.
namespace Utils {
export function log(msg: string) { console.log(msg); }
}
namespace Utils {
export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');Namespace com Função
Namespaces podem mesclar com funções, classes e enums. O namespace adiciona propriedades estáticas à função. Usado em Moment.js e bibliotecas similares.
function Counter() { Counter.count++; }
namespace Counter {
export let count = 0;
export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count); // 2Mesclando com Classes
Mesclar um namespace com uma classe adiciona membros estáticos e tipos aninhados. O namespace pode exportar interfaces que se tornam tipos aninhados.
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };Mesclagens Não Permitidas
Classes não podem mesclar com outras classes. Variáveis não podem mesclar. Funç ões mesclam como overloads. Enums podem mesclar com namespaces.
// Cannot merge classes
class A { x = 1; }
class A { y = 2; } // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }Type Narrowing
typeof & instanceof
typeof estreita tipos primitivos. instanceof estreita tipos de classe. TypeScript entende essas verificações e estreita o tipo em cada branch.
function process(value: string | number | Date) {
if (typeof value === 'string') return value.toUpperCase();
if (typeof value === 'number') return value.toFixed(2);
if (value instanceof Date) return value.toISOString();
}Operador in
O operador in verifica se uma propriedade existe, estreitando o tipo. Útil para discriminated unions com nomes de propriedade diferentes.
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
if ('meow' in animal) animal.meow();
else animal.bark();
}Discriminated Unions
Discriminated unions usam uma propriedade comum (discriminant) para estreitar tipos. switch no discriminant para verificação exaustiva. Padrão mais seguro para tipos variantes.
type Result =
| { status: 'success'; data: string }
| { status: 'error'; message: string };
function handle(r: Result) {
switch (r.status) {
case 'success': console.log(r.data); break;
case 'error': console.log(r.message); break;
}
}Type Predicates
Type predicates (x is T) habilitam funções de narrowing personalizadas. Retornar true estreita para T, false estreita para o tipo excluído. TypeScript confia no predicate cegamente.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) pet.swim();
else pet.fly();
}Assertion Functions
Assertion functions lançam se a condição falhar, estreitando o tipo para código subsequente. asserts x is T estreita para T. Elimina verificações de null redundantes.
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
assertDefined(user);
console.log(user.name); // User (not undefined)
}Template Literal Types
Template Literals Básicos
Template literal types criam padrões de string. Eles restringem strings para corresponder a um template. Habilita padrões type-safe para endpoints de API e nomes de evento.
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world'; // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';Uppercase & Lowercase
Tipos intrínsecos integrados transformam tipos de string literal. Combine com template literals para gerar nomes de evento type-safe e constantes.
type Upper = Uppercase<'hello'>; // 'HELLO'
type Lower = Lowercase<'WORLD'>; // 'world'
type Cap = Capitalize<'foo'>; // 'Foo'
type EventName = `on${Capitalize<'click'>}`; // 'onClick'Key Remapping
Key remapping (cláusula as) transforma chaves durante mapped types. Gera nomes getter/setter a partir de nomes de propriedade. Cria APIs type-safe a partir de interfaces.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }String Pattern Matching
Template literal types com infer podem fazer parse de strings em tempo de compilação. Split quebra uma string em uma tuple. Habilita manipulação de string type-safe.
type Split<S extends string, D extends string> =
S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>; // ['a', 'b', 'c']Tipagem de Sistema de Eventos
Template literal types com generics criam sistemas de eventos totalmente type-safe. O nome do evento determina o tipo de payload. on e emit aplicam tipos correspondentes.
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x)); // Typed!Keyword infer
Extrair Tipo de Retorno
infer declara uma variável de tipo dentro de um conditional type. Captura o tipo em uma posição específica. ReturnType é o equivalente integrado.
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>; // { name: string; age: number; }Extrair Tipo de Promise
infer extrai o tipo interno de uma Promise. DeepUnwrap desembrulha recursivamente Promises aninhadas. O Awaited<T> integrado faz isso em TypeScript moderno.
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>; // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>; // booleanExtrair Elemento de Array
infer E captura o tipo de elemento de um array. Para tuples, infer pode capturar posições específicas. Útil para trabalhar com coleções genéricas.
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>; // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>; // stringExtrair Parâmetros de Função
infer P captura a tuple de parâmetros de uma função. Parameters é o equivalente integrado. Útil para envolver funções enquanto preserva tipos.
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>; // [string, number]Múltiplos infer
Múltiplas variáveis infer podem capturar diferentes partes de um tipo simultaneamente. Habilita transformações de tipo complexas em um único conditional.
type FirstLast<T extends any[]> =
T extends [infer First, ...any[], infer Last]
? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>; // { first: 1; last: 4 }Variance
Covariância
Covariância permite que Dog[] seja atribuído a Animal[]. Arrays do TypeScript são covariantes, mas isso é unsound: empurrar um Animal em um Dog[] corrompe o array.
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs; // OK (covariant)
// animals.push(new Animal()); // Unsafe at runtimeContravariância
Contravariância significa que uma função que aceita Dog pode ser usada onde uma função que aceita Animal é esperada. Seguro porque um handler de Dog lida com qualquer Animal que seja um Dog.
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler; // OK with strictFunctionTypesBivariância
Sintaxe de método é bivariante. Sintaxe de propriedade de função é contravariante com strictFunctionTypes. Métodos são bivariantes para compatibilidade OO.
interface IFace {
method(x: Animal): void; // bivariant
fn: (x: Animal) => void; // contravariant
}
class Impl implements IFace {
method(x: Dog) {} // OK (bivariant)
fn = (x: Dog) => {} // Error (contravariant)
}Variância in/out
TypeScript 4.7+ suporta anotações de variância explícitas. in marca contravariante (consumers), out marca covariante (producers), in out marca invariante (ambos).
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }Tipos Invariantes
Tipos invariantes requerem correspondências exatas de tipo. Um tipo é invariante quando aparece em tanto posições de input quanto de output. Box<Dog> não pode ser atribuído a Box<Animal>.
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox; // Error: invariantBuilder Pattern
Fluent Builder
O padrão builder constrói objetos complexos passo a passo. Cada método retorna this para encadeamento. Útil para queries SQL, requisições HTTP e configuração.
class QueryBuilder {
private parts: string[] = [];
select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
build(): string { return this.parts.join(' '); }
}Builder Type-Safe
Builders type-safe usam conditional types para aplicar fields obrigatórios. build() apenas retorna Person quando hasName é true. Captura fields ausentes em tempo de compilação.
interface State { hasName: boolean; }
class Builder<S extends State> {
name(n: string): Builder<S & { hasName: true }> { return this as any; }
build(): S extends { hasName: true } ? Person : never { return {} as any; }
}Builder Imutável
Builders imutáveis criam uma nova instância para cada modificação. O type system rastreia todas as chaves adicionadas através de intersection types. Cada set retorna um novo tipo de builder.
class ImmutableBuilder<T extends object> {
constructor(private data: T) {}
set<K extends string, V>(key: K, value: V):
ImmutableBuilder<T & { [P in K]: V }> {
return new ImmutableBuilder({ ...this.data, [key]: value });
}
build(): T { return this.data; }
}Director Pattern
O Director encapsula sequências comuns de construção. Usa um builder para criar produtos padrão. Diferentes directors produzem variações diferentes.
class HTMLBuilder {
private html = '';
addTag(tag: string, content: string): this {
this.html += `<${tag}>${content}</${tag}>`; return this;
}
build(): string { return this.html; }
}
class Director {
buildPage(title: string, body: string): string {
return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
}
}Step Builder
Step builder aplica uma ordem específica de chamadas de método através do type system. Cada step retorna um tipo diferente com apenas o próximo método disponível.
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
constructor(private name: string) {}
age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
constructor(private name: string, private age: number) {}
build() { return { name: this.name, age: this.age }; }
}Testando com TypeScript
Jest com TypeScript
Use ts-jest ou @swc/jest para testes TypeScript. describe agrupa testes relacionados, it define casos de teste. expect cria asserções com matchers como toBe, toEqual.
import { sum } from './sum';
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('handles negatives', () => {
expect(sum(-1, -2)).toBe(-3);
});
});Teste de Tipos
expectTypeOf testa tipos em tempo de compilação. Verifica tipos de retorno, tipos de parâmetro e tipos de promise resolvidos. Falha o build se os tipos estiverem errados.
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
const fn = {} as unknown as GetUser;
expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});Mocking com Tipos
jest.Mocked<T> cria um mock tipado a partir de uma interface. jest.fn() cria funções mock com valores de retorno tipados. O mock é totalmente tipado.
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });Setup & Teardown de Teste
beforeAll executa uma vez antes de todos os testes, afterAll uma vez depois. beforeEach executa antes de cada teste, afterEach depois de cada um. Use para setup e limpeza.
describe('Database', () => {
beforeAll(async () => { db = createDatabase(); await db.connect(); });
afterAll(async () => { await db.disconnect(); });
beforeEach(async () => { await db.clear(); });
afterEach(() => { jest.restoreAllMocks(); });
});Property-Based Testing
Property-based testing gera entradas aleatórias para testar invariantes. fc.assert executa a propriedade múltiplas vezes. Captura edge cases que testes baseados em exemplo perdem.
import { fc } from 'fast-check';
describe('sort', () => {
it('preserves length', () => {
fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
return [...arr].sort().length === arr.length;
}));
});
});Armadilhas Comuns
any vs unknown
any desabilita verificação de tipo, ocultando bugs. unknown é type-safe: você deve estreitá-lo antes do uso. Use unknown para fontes não confiáveis (API, JSON.parse).
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
if (typeof data === 'object' && data !== null && 'foo' in data)
return (data as any).foo;
}Verificações de Propriedade Excess
TypeScript apenas verifica propriedades excess em object literals atribuídos diretamente. Via variável, a verificação é pulada. Use zod para validação de runtime mais rigorosa.
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true }; // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data; // OKEnum vs Union
Enums criam objetos de runtime com reverse mapping. Union types são zero-runtime e tree-shakeable. Prefira union types para código novo.
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }Structural Typing
TypeScript usa structural typing: tipos são compatíveis se as formas corresponderem. Admin é atribuível a User. Isso pode causar bugs lógicos. Branded types adicionam distinção nominal.
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin; // OK (structural)Perigos de Type Assertion
Type assertions (as) sobrescrevem TypeScript sem verificações de runtime. Use validação de runtime (zod, io-ts) para dados externos. safeParse retorna um resultado sem lançar.
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }Snippets de TypeScript relacionados
Copy-paste ready code for common tasks.
Funções Genéricas
Definir e usar funções genéricas.
Tipos Condicionais
Selecionar tipos com base em condições.
Tipos Mapeados
Construir novos tipos a partir de existentes.
Tipos Utilitários
Tipos utilitários embutidos do TypeScript.
Type Guards
Funções de type guard personalizadas.
Sobrecarga de Funções
Definir assinaturas de sobrecarga de função.
Decoradores
Decoradores de classe e método.
Enum
Enums numéricos, de string e const.
Herança de Interface
Herança e implementação de interface.
Classes Abstratas
Definir classes abstratas e métodos abstratos.
Namespaces
Organizar código usando namespaces.
Declarações de Módulo
Escrever declarações de tipo para bibliotecas JS.
Merge de Declarações
Mesclar múltiplas declarações com o mesmo nome.
Optional Chaining
Acessar com segurança propriedades profundas.
Nullish Coalescing
Usar um valor padrão apenas para null/undefined.
Inferência de Tipo
TypeScript infere tipos automaticamente.
const Assertions
Estreitar tipos usando as const.
Operador satisfies
Verificar tipo preservando o tipo mais estreito.
Palavra-chave infer
Extrair tipos dentro de tipos condicionais.
Tipos de Template Literal
Construir tipos com base em strings.
Was this helpful?