Deep dive em TypeScript — sistema de tipos estáticos para JavaScript. Para fundamentos da linguagem base, ver JavaScript Fundamentals. Para React com TypeScript, ver React. Para testes com TS, ver Testes em JavaScript.
O que é
TypeScript é um superset tipado de JavaScript criado pela Microsoft (2012, Anders Hejlsberg — criador do C#). Adiciona tipos estáticos opcionais ao JavaScript, compilando para JS puro (ou, desde Node 22.18+, rodando nativamente via type stripping).
Em 2026:
#1 linguagem no GitHub (ultrapassou JavaScript)
TypeScript 7 lançamento previsto mid-2026, com compilador reescrito em Go (10x mais rápido)
Node 22+ suporta TS nativamente via --experimental-strip-types
Default em novos projetos JavaScript sérios
Por que TypeScript:
Erros detectados em compile time, não em runtime
IDE intelligence — autocomplete, rename refactoring, go-to-definition
Documentação viva — tipos são docs que nunca desatualizam
Refactoring seguro — o compilador avisa onde você quebrou coisas
Contrato entre camadas — backend → OpenAPI → frontend com tipos gerados
Em entrevistas, o que diferencia um senior em TypeScript:
Structural typing — por que duck typing funciona em TS
Type narrowing — como TS “aprende” o tipo em runtime via type guards
Module resolution — paths, baseUrl, ESM vs CJS em TS
TypeScript vs JavaScript
Structural typing
TypeScript usa structural typing (duck typing): se dois tipos têm a mesma forma, são compatíveis.
type Point = { x: number; y: number };type Coord = { x: number; y: number; z?: number };function print(p: Point) { }const c: Coord = { x: 1, y: 2 };print(c); // OK — Coord tem pelo menos as propriedades de Point
Isso é diferente de Java/C# (nominal typing), onde dois tipos com a mesma estrutura mas nomes diferentes são incompatíveis.
Excess property checking
Literais de objeto passados diretamente são verificados estritamente:
print({ x: 1, y: 2, z: 3 }); // ERRO — z não está em Point
Mas se passar via variável, o check é relaxado:
const obj = { x: 1, y: 2, z: 3 };print(obj); // OK — structural match
Tipos básicos
// Primitivoslet nome: string = 'Maria';let idade: number = 30;let ativo: boolean = true;let nada: null = null;let indefinido: undefined = undefined;let big: bigint = 42n;let sym: symbol = Symbol('id');// Arrayslet numeros: number[] = [1, 2, 3];let nomes: Array<string> = ['a', 'b']; // sintaxe alternativa// Tupleslet par: [string, number] = ['Maria', 30];let comOpcional: [string, number?] = ['Maria'];let comRest: [string, ...number[]] = ['Maria', 1, 2, 3];let nomeado: [first: string, last: string] = ['Ana', 'Silva'];// Objetoslet user: { nome: string; idade: number } = { nome: 'Maria', idade: 30 };// any — desliga a verificação (evite)let qualquer: any = 'anything';// unknown — precisa narrowing antes de usarlet desconhecido: unknown = 'hello';// desconhecido.toUpperCase(); // ERROif (typeof desconhecido === 'string') { desconhecido.toUpperCase(); // OK — narrowed}// never — nunca acontece (funções que lançam, loops infinitos)function fail(msg: string): never { throw new Error(msg);}// void — função sem retornofunction log(msg: string): void { console.log(msg);}
any vs unknown vs never
any
unknown
never
Aceita qualquer valor
✅
✅
❌
Pode ser atribuído a qualquer tipo
✅
❌ (só any/unknown)
✅
Permite qualquer operação
✅
❌ (precisa narrowing)
N/A
Uso
evite
input não confiável
casos impossíveis
Regra: substitua any por unknown sempre que possível. unknown força você a verificar o tipo antes de usar.
Interfaces e Type aliases
Ambos definem formas. Diferenças:
// Interfaceinterface User { name: string; age: number;}// Type aliastype UserType = { name: string; age: number;};
Quando usar interface
Definir formas de objetos que outras partes podem estender
Declaration merging — útil para estender tipos de bibliotecas
interface User { name: string;}interface User { age: number; // merged — User agora tem name + age}// Exemplo real — estender windowdeclare global { interface Window { myApp: { version: string }; }}
Quando usar type
Unions e intersections
Tuples
Mapped types
Template literal types
Primitivos renomeados
type Status = 'active' | 'inactive' | 'pending';type Point = [number, number];type UserId = string;type Keys = keyof User;
Regra prática
Use interface para “shapes” (APIs públicas, objetos); type para tudo mais (unions, utility-driven, etc.). Inconsistência aqui importa menos que outras decisões.
type ID = string | number;function format(id: ID): string { if (typeof id === 'string') { return id.toUpperCase(); // narrowed para string } return id.toString(); // narrowed para number}
Intersection types (&)
type Named = { name: string };type Aged = { age: number };type Person = Named & Aged;const p: Person = { name: 'Maria', age: 30 };
Cuidado com conflitos:
type A = { x: string };type B = { x: number };type C = A & B; // x: never — intersecção impossível
Discriminated unions — o pattern mais importante
type Result<T> = | { success: true; data: T } | { success: false; error: string };function handle<T>(result: Result<T>): T { if (result.success) { return result.data; // narrowed para { success: true; data: T } } else { throw new Error(result.error); // narrowed para { success: false; error: string } }}
Uso em state machines:
type LoadingState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: User } | { status: 'error'; error: string };function render(state: LoadingState) { switch (state.status) { case 'idle': return 'Waiting...'; case 'loading': return 'Loading...'; case 'success': return `Hello, ${state.data.name}`; case 'error': return `Error: ${state.error}`; }}
Exhaustiveness checking:
function render(state: LoadingState): string { switch (state.status) { case 'idle': return '...'; case 'loading': return '...'; case 'success': return '...'; // Falta 'error' default: const _exhaustive: never = state; // ERRO — state é LoadingState<error> return _exhaustive; }}
Type narrowing
TypeScript usa control flow analysis para restringir tipos baseado em checks em runtime.
typeof guards
function process(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); // string } return value.toFixed(2); // number}
type Cat = { meow: () => void };type Dog = { bark: () => void };function speak(animal: Cat | Dog) { if ('meow' in animal) { animal.meow(); // Cat } else { animal.bark(); // Dog }}
Discriminant property
type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; side: number };function area(s: Shape): number { switch (s.kind) { case 'circle': return Math.PI * s.radius ** 2; case 'square': return s.side ** 2; }}
Custom type guards (user-defined)
interface Cat { meow: () => void; }interface Dog { bark: () => void; }function isCat(animal: Cat | Dog): animal is Cat { return 'meow' in animal;}function speak(animal: Cat | Dog) { if (isCat(animal)) { animal.meow(); // TS sabe que é Cat } else { animal.bark(); // TS sabe que é Dog }}
Assertion functions (TS 3.7+)
function assertDefined<T>(x: T | undefined): asserts x is T { if (x === undefined) throw new Error('undefined!');}function process(user?: User) { assertDefined(user); console.log(user.name); // TS sabe que user é User}
Non-null assertion (!)
const user = users.find(u => u.id === id)!; // força não-null// Use com cuidado — você promete ao compilador que não é null
Regra:! é um “confio em mim” — evite quando possível. Prefira narrowing explícito.
Generics
Tipos parametrizados — funções e tipos que trabalham com múltiplos tipos.
Funções genéricas
function identity<T>(value: T): T { return value;}identity<string>('hello'); // explicitidentity(42); // inferred como number// Multi-parâmetrosfunction pair<A, B>(a: A, b: B): [A, B] { return [a, b];}const p = pair('Maria', 30); // [string, number]
Constraints
interface HasLength { length: number;}function longer<T extends HasLength>(a: T, b: T): T { return a.length >= b.length ? a : b;}longer('hello', 'world'); // OK — string tem lengthlonger([1, 2], [3, 4, 5]); // OK — array tem length// longer(1, 2); // ERRO — number não tem length
Default generic types
function create<T = string>(value: T): T[] { return [value];}create(42); // T = numbercreate('hello'); // T = stringcreate(); // T = string (default)
Generic classes
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 s = new Stack<number>();s.push(1);s.push(2);s.pop(); // 2
type IsString<T> = T extends string ? true : false;type A = IsString<'hello'>; // truetype B = IsString<42>; // false// Extrair tipo de arraytype ElementOf<T> = T extends (infer U)[] ? U : never;type Str = ElementOf<string[]>; // stringtype Num = ElementOf<number[]>; // number// Distributive conditional typestype ToArray<T> = T extends any ? T[] : never;type Mixed = ToArray<string | number>; // string[] | number[]
Infer
Extrai tipos de dentro de outros:
// Extrair tipo de retorno de funçãotype ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;type R = ReturnOf<() => Promise<User>>; // Promise<User>// Extrair tipo de Promisetype Awaited<T> = T extends Promise<infer U> ? U : T;type A = Awaited<Promise<string>>; // stringtype B = Awaited<Promise<Promise<number>>>; // Promise<number> (não recursivo)
Utility types
TypeScript inclui vários utility types. Os mais importantes:
Partial, Required, Readonly
interface User { id: number; name: string; email: string;}// Todos os campos opcionaistype UserUpdate = Partial<User>;// { id?: number; name?: string; email?: string }// Todos os campos obrigatórios (remove optional)type RequiredUser = Required<UserUpdate>;// Todos readonlytype FrozenUser = Readonly<User>;// frozen.name = 'x'; // ERRO
Pick, Omit
// Só os campos escolhidostype UserPreview = Pick<User, 'id' | 'name'>;// { id: number; name: string }// Todos exceto os escolhidostype UserWithoutEmail = Omit<User, 'email'>;// { id: number; name: string }
// Fazer todos readonlytype Readonly<T> = { readonly [K in keyof T]: T[K];};// Fazer todos opcionaistype Partial<T> = { [K in keyof T]?: T[K];};// Adicionar prefixo nas chavestype Prefixed<T, Prefix extends string> = { [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];};type User = { name: string; age: number };type PrefixedUser = Prefixed<User, 'user'>;// { userName: string; userAge: number }
Key remapping (TS 4.1+)
// Remover chaves por padrãotype RemoveField<T, K extends keyof T> = { [P in keyof T as P extends K ? never : P]: T[P];};// Criar getterstype Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};type UserGetters = Getters<User>;// { getName: () => string; getAge: () => number }
const arr = ['a', 'b', 'c'];// Sem a flagconst x = arr[0]; // stringarr[999].toUpperCase(); // compila, mas crasha em runtime// Com a flagconst y = arr[0]; // string | undefinedarr[999].toUpperCase(); // ERRO — precisa verificar
Recomendo ativar sempre. Força verificação de bounds.
exactOptionalPropertyTypes
interface User { name: string; middleName?: string; // string | undefined}// Sem a flagconst u: User = { name: 'Maria', middleName: undefined }; // OK// Com a flagconst u: User = { name: 'Maria', middleName: undefined }; // ERRO// middleName é string | missing, não string | undefinedconst u2: User = { name: 'Maria' }; // OK
Distingue entre “propriedade ausente” e “propriedade = undefined”.
Configuração tsconfig.json
Exemplo para projeto moderno (Node 22+, ESM, strict):
Zod schema é a source of truth — tipo é inferido de lá.
3. OpenAPI → tipos:
Backend (Spring Boot) gera OpenAPI via SpringDoc. Frontend consome com openapi-typescript que gera tipos. Quando o backend muda um campo, o TypeScript quebra no frontend — erro de compilação, não runtime. Esse loop economiza tempo enorme.
4. Result types em domain code:
Services retornam Result<T, DomainError> em vez de throwing. Força o caller a lidar com erros. Nos boundaries (controllers), converto para HTTP response.
5. Discriminated unions para state:
Componentes React com LoadingState = { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: E }. Switch exhaustivo no render.
6. Branded types para IDs:
type Brand<K, T> = K & { readonly __brand: T };type UserId = Brand<string, 'UserId'>;type OrderId = Brand<string, 'OrderId'>;// UserId e OrderId não são intercambiáveis mesmo sendo ambos strings
Evita trocar IDs no código — erro de compile-time.
7. import type sempre:
Imports de tipos com import type para garantir que são removidos do JS. Melhora tree shaking.
Função helper antiga tinha tipo function parse(json: string): any. Esse any foi propagado por toda a aplicação. Um campo renomeado no backend quebrou em runtime — nenhum erro de compile. Refactor: substituí por unknown + Zod validation. Compilador encontrou dezenas de lugares onde o código assumia shape errado. Bugs escondidos descobertos por tipos.
Solução: (Object.keys(obj) as (keyof typeof obj)[]). Ou usar for (const key in obj) que narra melhor.
A lição principal: TypeScript é uma ferramenta de pensamento. Quando os tipos estão difíceis de expressar, é sinal de que o design está ruim — não de que TS está atrapalhando. Domine o sistema de tipos avançado (generics, conditionals, mapped types) e você modela domínios complexos com segurança enorme.
How to explain in English
“TypeScript is a typed superset of JavaScript that I use by default in any serious project. The value isn’t just catching typos — it’s using types as a thinking tool. When types are hard to express, it signals the design needs work.
My baseline tsconfig is strict mode plus several additional flags: noUncheckedIndexedAccess so array access returns T | undefined, exactOptionalPropertyTypes to distinguish between missing and undefined, and noImplicitReturns to catch logic gaps. Without these, you only get partial safety.
I leverage the type system heavily. Discriminated unions for state — a LoadingState type with variants for idle, loading, success, and error makes UI exhaustive switches automatic. Branded types for IDs prevent accidentally passing a UserId where an OrderId is expected. Generic constraints and conditional types for library code. Template literal types for API paths.
Since TypeScript is compile-time only, I use Zod for runtime validation of anything external — request bodies, environment variables, responses from third-party APIs. Zod schemas are the source of truth, and types are inferred from them. That loop catches mismatches between what the runtime sees and what the types claim.
For frontend-backend contracts, I generate TypeScript types from OpenAPI schemas. When the backend changes a field, the frontend fails to compile — not at runtime. That feedback loop is invaluable.
I avoid any religiously — it disables type checking entirely. When I don’t know a type, I use unknown and narrow before using. as assertions are ‘trust me’ markers and I treat them as a last resort.
For async, I sometimes use Result types instead of throw. A function returning Result<User, UserError> forces the caller to handle both cases explicitly. It’s more functional and harder to forget errors.”
Frases úteis em entrevista
“I use TypeScript as a thinking tool, not just type safety.”
“Strict mode isn’t enough — noUncheckedIndexedAccess and exactOptionalPropertyTypes catch real bugs.”
“Discriminated unions are my go-to for state machines and Result types.”
“I use Zod for runtime validation at system boundaries.”
“Types are compile-time — always validate external data at runtime.”
“any disables checking. unknown forces narrowing. I prefer unknown.”
“Branded types prevent accidentally passing the wrong ID type.”
“I generate TypeScript types from OpenAPI for backend contracts.”
“I avoid enums — const objects with as const are tree-shakeable and give you literal types.”
“Type assertions (as) are ‘trust me’ markers — last resort.”
Key vocabulary
tipagem estrutural → structural typing
estreitamento de tipo → type narrowing
proteção de tipo → type guard
tipo utilitário → utility type
tipo mapeado → mapped type
tipo condicional → conditional type
tipo literal de template → template literal type
união discriminada → discriminated union
interseção → intersection
genérico → generic
restrição → constraint
inferência → inference
asserção de tipo → type assertion
declaração de tipo → type declaration
arquivo de declaração → declaration file
módulo ambiente → ambient module
validação em tempo de execução → runtime validation