Se você escreve JavaScript e ainda não usa TypeScript, está abrindo mão da ferramenta mais eficaz para evitar bugs em produção. TypeScript não é uma linguagem diferente — é JavaScript com tipagem estática. O compilador te avisa dos erros antes do código rodar, não depois. Neste guia, apresento tudo que você precisa para começar a usar TypeScript em projetos reais.
Por que TypeScript
JavaScript é dinâmico — qualquer variável pode ser qualquer coisa a qualquer momento. Isso é flexível, mas perigoso. Você descobre erros só quando o código roda e o usuário vê o erro.
// JavaScript: isso roda sem erro
function calculateTotal(price, quantity) {
return price * quantity
}
calculateTotal('10', 2) // Retorna '102' (string), não 20
calculateTotal(10) // Retorna NaN, sem avisoTypeScript pega esses erros antes de rodar:
// TypeScript: o compilador avisa
function calculateTotal(price: number, quantity: number): number {
return price * quantity
}
calculateTotal('10', 2) // ✗ Erro: Argument of type 'string' is not assignable to 'number'
calculateTotal(10) // ✗ Erro: Expected 2 arguments, but got 1Tipos básicos
Os tipos primitivos do TypeScript mapeiam diretamente os tipos do JavaScript:
// Primitivos
let name: string = 'Vinicius'
let age: number = 24
let isActive: boolean = true
// Arrays
let tags: string[] = ['react', 'next', 'typescript']
let scores: number[] = [95, 87, 92]
// Objetos inline
let user: { name: string; email: string } = {
name: 'Vinicius',
email: 'vini@email.com'
}Na prática, você quase nunca precisa tipar variáveis explicitamente — o TypeScript infere o tipo automaticamente:
// Inferência automática — o TS sabe que é string
let name = 'Vinicius' // tipo: string
let age = 24 // tipo: number
let tags = ['react'] // tipo: string[]
// Tipar explicitamente só quando necessário
let data: unknown = await fetchFromAPI()Interfaces e Types
Para objetos mais complexos, use interface ou type. A diferença prática é mínima — use interface para objetos e type para unions e tipos compostos:
// Interface — para objetos
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
avatar?: string // campo opcional
}
// Type — para unions e composições
type Status = 'pending' | 'confirmed' | 'failed' | 'refunded'
type ApiResponse<T> = {
data: T
success: boolean
error: string | null
}Interfaces podem ser estendidas, o que é útil para herança de tipos:
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
}
interface User extends BaseEntity {
name: string
email: string
}
interface Order extends BaseEntity {
userId: string
total: number
status: Status
}
// User agora tem id, createdAt, updatedAt, name, emailFunções tipadas
Tipar funções é onde TypeScript mais brilha. O compilador valida os argumentos e o retorno:
// Parâmetros e retorno tipados
function createUser(name: string, email: string): User {
return {
id: crypto.randomUUID(),
name,
email,
role: 'user',
createdAt: new Date(),
updatedAt: new Date()
}
}
// Arrow function
const formatPrice = (cents: number): string => {
return `R$ ${(cents / 100).toFixed(2)}`
}
// Função assíncrona
async function getUser(id: string): Promise<User | null> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) return null
return res.json()
}Generics: tipos reutilizáveis
Generics permitem criar funções e tipos que funcionam com qualquer tipo, mantendo a segurança. É o conceito mais poderoso do TypeScript:
// Sem generics — precisaria de uma função por tipo
function getFirstString(arr: string[]): string | undefined {
return arr[0]
}
function getFirstNumber(arr: number[]): number | undefined {
return arr[0]
}
// Com generics — uma função para todos os tipos
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
getFirst(['a', 'b', 'c']) // tipo: string | undefined
getFirst([1, 2, 3]) // tipo: number | undefined
getFirst<User>(users) // tipo: User | undefinedGenerics são essenciais para funções de API, hooks do React e qualquer código reutilizável:
// Resposta de API genérica
async function fetchAPI<T>(endpoint: string): Promise<ApiResponse<T>> {
const res = await fetch(`/api${endpoint}`)
const data = await res.json()
return data as ApiResponse<T>
}
// Uso — o TS sabe o tipo do retorno
const { data: user } = await fetchAPI<User>('/users/123')
// user é do tipo User
const { data: orders } = await fetchAPI<Order[]>('/orders')
// orders é do tipo Order[]Utility Types: transformando tipos
TypeScript vem com utility types que transformam tipos existentes. Esses são os que uso diariamente:
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// Partial — todos os campos se tornam opcionais
type UpdateUser = Partial<User>
// { id?: string; name?: string; email?: string; role?: ... }
// Pick — seleciona campos específicos
type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }
// Omit — remove campos específicos
type CreateUser = Omit<User, 'id'>
// { name: string; email: string; role: 'admin' | 'user' }
// Record — cria objeto tipado
type UserMap = Record<string, User>
// { [key: string]: User }O uso mais comum é em formulários e APIs — onde você precisa de variações do mesmo tipo:
// API: criar usuário (sem id, o banco gera)
async function createUser(data: Omit<User, 'id'>): Promise<User> {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
})
return res.json()
}
// API: atualizar parcialmente (todos os campos opcionais)
async function updateUser(id: string, data: Partial<User>): Promise<User> {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data)
})
return res.json()
}Enums vs Union Types
Enums existem no TypeScript mas, na prática, union types são mais simples e não geram código JavaScript extra:
// ❌ Enum — gera código JS extra, mais complexo
enum PaymentStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
FAILED = 'failed',
}
// ✅ Union type — mais simples, zero overhead
type PaymentStatus = 'pending' | 'confirmed' | 'failed'
// Ambos funcionam igual na validação:
function processPayment(status: PaymentStatus) {
switch (status) {
case 'pending':
return 'Aguardando...'
case 'confirmed':
return 'Confirmado!'
case 'failed':
return 'Falhou.'
}
}Type guards: refinando tipos
Quando um valor pode ser de vários tipos, type guards ajudam o TypeScript a entender qual tipo é em cada momento:
type Result = { success: true; data: User } | { success: false; error: string }
function handleResult(result: Result) {
if (result.success) {
// TS sabe que result.data existe aqui
console.log(result.data.name)
} else {
// TS sabe que result.error existe aqui
console.error(result.error)
}
}
// Type guard customizado
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
)
}
const data: unknown = await fetchSomething()
if (isUser(data)) {
// TS sabe que data é User aqui
console.log(data.email)
}Configuração: tsconfig.json
O tsconfig.json define como o TypeScript se comporta no projeto. Para projetos modernos com Next.js ou Node.js, essa é a base que recomendo:
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}O mais importante é "strict": true — ativa todas as verificações de tipo. Projetos sem strict mode perdem a maior parte do valor do TypeScript.
Padrões que uso em produção
Depois de usar TypeScript em todos os projetos, esses são os padrões que mais aplico no dia a dia:
- **Nunca usar
any** — useunknownquando não sabe o tipo e faça type guard para validar - Inferência primeiro — deixe o TS inferir o tipo sempre que possível, tipar explicitamente só quando necessário
- Interfaces para props de componentes — toda prop de componente React deve ter uma interface
- Utility types para APIs —
Omit<User, 'id'>para criação,Partial<User>para update - Union types ao invés de enums — mais simples, sem overhead de runtime
- **
as const** — para arrays e objetos literais que não devem mudar
// as const — transforma valores em tipos literais
const ROLES = ['admin', 'user', 'moderator'] as const
type Role = typeof ROLES[number] // 'admin' | 'user' | 'moderator'
// Props de componente React
interface ButtonProps {
label: string
variant?: 'primary' | 'secondary' | 'ghost'
onClick: () => void
disabled?: boolean
}
function Button({ label, variant = 'primary', onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={variant}>
{label}
</button>
)
}Conclusão
TypeScript não é sobre escrever mais código — é sobre escrever código que o compilador consegue validar. Os tipos documentam a intenção, as interfaces definem contratos entre módulos, e os generics permitem reutilização sem perder segurança. Se você vem do JavaScript, a curva de aprendizado é suave: comece tipando funções e props, depois avance para generics e utility types. O ganho em produtividade e confiança no código aparece rápido.
