Vinicius Aguiar
TypeScript

Iniciando no TypeScript: guia prático para devs JavaScript

16 de abril de 2026 · 12 min de leitura

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 aviso

TypeScript 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 1

Tipos 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, email

Funçõ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 | undefined

Generics 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** — use unknown quando 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 APIsOmit<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.