Voltar ao blog
Tutorial

TypeScript pra quem vem do JavaScript puro

Por Flávio Emanuel · · 10 min de leitura

Começar em TypeScript quando você vem de JavaScript puro é tipo aprender a dirigir com freio. No começo sente que tá lento. Depois percebe que freio salva sua vida.

Tenho 8 anos desenvolvendo. 6 anos fiz JavaScript puro. Últimos 2 anos TypeScript em tudo. Não volto.

A diferença é que TS descobre erros antes de usuário descobrir. Tipo um tipo de seguro pra código.

Por que TS virou padrão

2026 chegou e você não consegue job sem TS. Por quê?

Porque empresa grande sabe que código sem type é bomba-relógio. Deixa crescer, dois anos depois pega bug absurdo que ninguém consegue achar. Investimento em type é investimento em sanidade mental.

Dev solo também tá migrando. Por quê? Porque quando você tem 5 cliente com projeto diferente, tipo refatorar em JavaScript puro é pesadelo. Você troca coisa e quebra tudo sem saber.

TS força você a pensar antes de digitar. Parece lento no começo, depois economiza semanas de debugging.

Os 5 conceitos que importam

Vou simplificar bem. TS é grande, mas você não precisa aprender tudo.

1. Types básicos

let nome: string = "João";
let idade: number = 25;
let ativo: boolean = true;
let qualquerCoisa: any = "cuidado com this";

Tipo assim. Você fala qual é o tipo de variável. Se tentar atribuir errado, TypeScript reclama antes de executar.

Sem type:

let user = "João";
user = user.toUpperCase(); // ok
user = 42; // ué, ok, agora é number
user.toUpperCase(); // crash em runtime

Com type:

let user: string = "João";
user = 42; // erro no editor já

Salva.

2. Interfaces

Interface é tipo um contrato. Diz qual é a forma de um objeto.

interface User {
  nome: string;
  idade: number;
  email?: string; // opcional
}

function criar(user: User) {
  return `Criando ${user.nome}`;
}

criar({ nome: "João", idade: 25 }); // ok
criar({ nome: "João" }); // erro, falta idade
criar({ nome: "João", idade: "25" }); // erro, idade não é number

Usado em tudo que eu faço. Props de componente? Interface. Response de API? Interface. Função que recebe objeto? Interface.

Economiza bug de “ué, qual é o shape desse objeto mesmo?“

3. Generics

Genéricos são tipo um template. Você define uma função que funciona com qualquer tipo, mas mantém type safety.

function primeiro<T>(lista: T[]): T {
  return lista[0];
}

const numeroPrimeiro = primeiro([1, 2, 3]); // tipo: number
const stringPrimeira = primeiro(["a", "b"]); // tipo: string

Tipo assim. Função faz a mesma coisa, mas tipo é inferido automaticamente.

Uso bastante com Supabase. Quando você faz query, tipo de retorno é genérico. TS descobre qual é o tipo sem você especificar.

4. Union types

Union é quando variável pode ser múltiplos tipos.

type Status = "ativo" | "inativo" | "pendente";

function processar(status: Status) {
  if (status === "ativo") {
    // faz coisa A
  } else if (status === "inativo") {
    // faz coisa B
  }
}

Parece simples. Mas evita erros absurdos tipo:

processar("ativissimo"); // erro! só aceita ativo, inativo ou pendente

Muito útil pra estados de UI. Loading, error, success.

5. Type inference

TS consegue adivinhar o tipo sem você especificar sempre.

const nome = "João"; // TS sabe que é string
const numeros = [1, 2, 3]; // TS sabe que é number[]

Você não precisa escrever const nome: string = "João". TS descobre.

Isso torna TypeScript menos verboso que parece. No dia a dia, você escreve type em interface, função e pronto. O resto TS cuida.

Setup com Astro

Astro já vem com TS pronto. Só precisa criar arquivo .ts em vez de .js.

// src/api/agendamentos.ts
interface Agendamento {
  id: string;
  paciente: string;
  data: Date;
  duracao: number; // minutos
}

export async function buscarAgendamentos(clinicaId: string): Promise<Agendamento[]> {
  const response = await fetch(`/api/agendamentos?clinicaId=${clinicaId}`);
  return response.json();
}

Em componente Astro:

---
import { buscarAgendamentos } from '../api/agendamentos';

const agendamentos = await buscarAgendamentos("family-pilates");
---

<div>
  {agendamentos.map(ag => (
    <div>{ag.paciente} - {ag.data.toLocaleDateString()}</div>
  ))}
</div>

TS valida tipo de agendamentos automaticamente. Se você tentar acessar propriedade que não existe, editor reclama.

Setup com React + Astro

Se você tá usando React Islands no Astro:

// src/components/AgendadorForm.tsx
interface AgendadorFormProps {
  clinicaId: string;
  onSucesso?: (agendamento: any) => void;
}

export default function AgendadorForm({ clinicaId, onSucesso }: AgendadorFormProps) {
  const [carregando, setCarregando] = React.useState(false);

  async function agendar(data: FormData) {
    setCarregando(true);
    // implementação
  }

  return <form onSubmit={agendar}>...</form>;
}

React + TS é melhor que React + JavaScript puro. Props são validadas, estado tem tipo.

Erro comum: qualquer coisa é any

Novo dev em TS faz isso:

function processar(dados: any) {
  return dados.qualquerCoisa;
}

Aí não ganha nada. any é como voltar a JavaScript puro.

Melhor:

interface Dados {
  qualquerCoisa: string;
}

function processar(dados: Dados) {
  return dados.qualquerCoisa;
}

Tome como regra: se você escreve any, é porque não entendeu a estrutura ainda. Volta, entende, volta sem any.

Tempo de curva

Primeira semana é frustrante. Você digita coisa simples e TS reclama de tipo.

Segundo semana você começa a entender. Terceira semana você está escrevendo tipo antes de código.

Quarta semana você não consegue escrever JavaScript puro de novo.

Com React vs Astro, falei sobre quando usar cada um. TS funciona nos dois.

Se você faz projeto novo, use TS desde o dia 1. Não é overhead, é seguro.

Projeto antigo em JavaScript puro? Não precisa refatorar tudo. Comece a escrever arquivo novo em TS. Deixa JavaScript antigo como está. Gradualmente migra.

  • Instalar extensão de TS no editor (TypeScript Vue Plugin)
  • Criar arquivo .ts simples com tipos básicos
  • Experimentar interface com objeto de verdade
  • Usar generic em função reutilizável
  • Adicionar Union types pra estados
  • Configurar tsconfig.json do projeto
  • Migrar um componente completo pra TS

TypeScript é investimento que paga dividendo todo dia.

Tipo discriminado (discriminated union)

Conceito avançado que vale a pena aprender:

type ApiResponse =
  | { status: 'success'; data: string }
  | { status: 'error'; message: string }
  | { status: 'loading' }

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    console.log(response.data); // TS sabe que data existe aqui
  } else if (response.status === 'error') {
    console.log(response.message); // TS sabe que message existe aqui
  }
  // response.data aqui? TS reclama, porque pode não existir
}

TypeScript é inteligente: baseado no value de um campo, ele conhece os outros campos.

Uso muito em Supabase queries onde preciso diferenciar sucesso/erro:

type SupabaseResult<T> =
  | { success: true; data: T }
  | { success: false; error: string }

async function fetchUser(id: string): Promise<SupabaseResult<User>> {
  const { data, error } = await supabase
    .from('users')
    .select('*')
    .eq('id', id)
    .single()

  if (error) {
    return { success: false, error: error.message }
  }

  return { success: true, data }
}

Depois no código:

const result = await fetchUser('123')

if (result.success) {
  console.log(result.data.name) // TypeScript sabe que data existe
} else {
  console.log(result.error) // TypeScript sabe que error existe
}

Zero runtime overhead, seguro total.

Utility types: aproveitar o poder

TypeScript tem utility types que salvam vida:

interface User {
  id: string
  name: string
  email: string
  password: string // ← NUNCA retorna isso
}

// Omit: remove campos
type PublicUser = Omit<User, 'password'>

// Pick: pega só alguns campos
type UserPreview = Pick<User, 'name' | 'email'>

// Partial: torna tudo opcional
type UserUpdate = Partial<User>

// Record: cria objeto com chaves específicas
type UserRole = Record<'admin' | 'user' | 'guest', boolean>

// Readonly: torna tudo imutável
type ReadonlyUser = Readonly<User>

Uso isso constantemente:

export async function getPublicUser(id: string): Promise<PublicUser> {
  const user = await fetchUser(id)
  // Retornar 'password'? TS reclama
  return { id: user.id, name: user.name, email: user.email }
}

TypeScript previne que você acidentalmente exponha dados sensíveis.

Keyof: tipo-safe access

function getValue<T>(obj: T, key: keyof T) {
  return obj[key]
}

const user = { name: 'João', age: 25 }

getValue(user, 'name') // ok
getValue(user, 'telefone') // erro! não existe

Tipo-safe de verdade. Você não consegue acessar propriedade que não existe.

Conditional types: tipo avançado

Parece intimidador mas é poderoso:

type IsString<T> = T extends string ? true : false

type A = IsString<'hello'> // true
type B = IsString<42> // false

Uso real: API que retorna tipos diferentes baseado em input:

type ApiCall<T extends 'user' | 'post'> =
  T extends 'user' ? User : Post

function call<T extends 'user' | 'post'>(type: T): ApiCall<T> {
  // implementação
}

const user = call('user') // TypeScript sabe que é User
const post = call('post') // TypeScript sabe que é Post

TypeScript inferiu tipo baseado em parâmetro. Magia.

Debugging de tipos

Às vezes tipo tá estranho. Como debugar?

type Suspicious = SomeComplexType

// TypeScript vai mostrar qual é o tipo
type Check = Suspicious

No editor, passe mouse em Check e TypeScript mostra o tipo expandido. Muito útil pra entender o que tá acontecendo.

Outra tática: usar never pra forçar erro se não tá certo:

type IsAny<T> = 0 extends (1 & T) ? true : false

const test: IsAny<any> = true // ok
const test2: IsAny<string> = true // erro

Performance: TypeScript pode desacelerar build

Se você tem tipos muito complexos, build fica lento.

Tática: use as const quando souber que tipo não vai mudar:

// Sem as const, TypeScript tenta ser genérico
const config = { port: 3000, host: 'localhost' } // tipo: { port: number, host: string }

// Com as const, TypeScript é específico
const config = { port: 3000, host: 'localhost' } as const // tipo: { port: 3000, host: 'localhost' }

Com as const, TypeScript não gasta tempo generalizando. Build fica mais rápido.

Evitar any: regra número 1

any é escape hatch. Use só quando realmente não consegue tipar:

// Razoável
const data: any = JSON.parse(response)

// Ruim
const result: any = await fetchData() // Você deveria saber o tipo

Se você tá usando any em função que você controla, volta e tipa direito.

Com Core Web Vitals falei sobre performance. TypeScript também importa: código bem tipado é código rápido.

  • Instalar TypeScript e criar primeiro arquivo .ts
  • Aprender interfaces vs types
  • Praticar generics com função reutilizável
  • Usar discriminated unions em projeto real
  • Aprender utility types (Pick, Omit, Partial)
  • Debugar tipos complexos com hover no editor
  • Rodar TypeScript compiler e ver erros
  • Migrar um componente de .js pra .ts

TypeScript no começo é overhead. Depois é seguro.

Próximo passo

Precisa de um dev que entrega de verdade?

Seja pra um projeto pontual, reforço no time, ou parceria de longo prazo. Vamos conversar.

Falar no WhatsApp

Respondo em até 2h durante horário comercial.