Si escribes JavaScript y todavía no usas TypeScript, estás renunciando a la herramienta más eficaz para evitar bugs en producción. TypeScript no es un lenguaje diferente — es JavaScript con tipado estático. El compilador te avisa de los errores antes de que el código se ejecute, no después. En esta guía, presento todo lo que necesitas para empezar a usar TypeScript en proyectos reales.
Por qué TypeScript
JavaScript es dinámico — cualquier variable puede ser cualquier cosa en cualquier momento. Esto es flexible pero peligroso. Solo descubres errores cuando el código se ejecuta y el usuario ve el error.
// JavaScript: esto corre sin error
function calculateTotal(price, quantity) {
return price * quantity
}
calculateTotal('10', 2) // Retorna '102' (string), no 20
calculateTotal(10) // Retorna NaN, sin avisoTypeScript atrapa estos errores antes de ejecutar:
// TypeScript: el compilador te avisa
function calculateTotal(price: number, quantity: number): number {
return price * quantity
}
calculateTotal('10', 2) // ✗ Error: Argument of type 'string' is not assignable to 'number'
calculateTotal(10) // ✗ Error: Expected 2 arguments, but got 1Tipos básicos
Los tipos primitivos de TypeScript mapean directamente los tipos de 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'
}En la práctica, casi nunca necesitas tipar variables explícitamente — TypeScript infiere el tipo automáticamente:
// Inferencia automática — TS sabe que es string
let name = 'Vinicius' // tipo: string
let age = 24 // tipo: number
let tags = ['react'] // tipo: string[]
// Tipar explícitamente solo cuando es necesario
let data: unknown = await fetchFromAPI()Interfaces y Types
Para objetos más complejos, usa interface o type. La diferencia práctica es mínima — usa interface para objetos y type para unions y tipos compuestos:
// Interface — para objetos
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
avatar?: string // campo opcional
}
// Type — para unions y composiciones
type Status = 'pending' | 'confirmed' | 'failed' | 'refunded'
type ApiResponse<T> = {
data: T
success: boolean
error: string | null
}Las interfaces pueden extenderse, lo que es útil para herencia 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 ahora tiene id, createdAt, updatedAt, name, emailFunciones tipadas
Tipar funciones es donde TypeScript más brilla. El compilador valida los argumentos y el retorno:
// Parámetros y 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)}`
}
// Función así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 reutilizables
Generics permiten crear funciones y tipos que funcionan con cualquier tipo, manteniendo la seguridad. Es el concepto más poderoso de TypeScript:
// Sin generics — necesitaría una función por tipo
function getFirstString(arr: string[]): string | undefined {
return arr[0]
}
function getFirstNumber(arr: number[]): number | undefined {
return arr[0]
}
// Con generics — una función para todos los 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 son esenciales para funciones de API, hooks de React y cualquier código reutilizable:
// Respuesta 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 — TS sabe el tipo del retorno
const { data: user } = await fetchAPI<User>('/users/123')
// user es de tipo User
const { data: orders } = await fetchAPI<Order[]>('/orders')
// orders es de tipo Order[]Utility Types: transformando tipos
TypeScript viene con utility types que transforman tipos existentes. Estos son los que uso diariamente:
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// Partial — todos los campos se vuelven opcionales
type UpdateUser = Partial<User>
// { id?: string; name?: string; email?: string; role?: ... }
// Pick — selecciona campos específicos
type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }
// Omit — remueve campos específicos
type CreateUser = Omit<User, 'id'>
// { name: string; email: string; role: 'admin' | 'user' }
// Record — crea objeto tipado
type UserMap = Record<string, User>
// { [key: string]: User }El uso más común es en formularios y APIs — donde necesitas variaciones del mismo tipo:
// API: crear usuario (sin id, la base de datos lo genera)
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: actualizar parcialmente (todos los campos opcionales)
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
Los enums existen en TypeScript pero en la práctica, los union types son más simples y no generan código JavaScript extra:
// ❌ Enum — genera código JS extra, más complejo
enum PaymentStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
FAILED = 'failed',
}
// ✅ Union type — más simple, zero overhead
type PaymentStatus = 'pending' | 'confirmed' | 'failed'
// Ambos funcionan igual para validación:
function processPayment(status: PaymentStatus) {
switch (status) {
case 'pending':
return 'Esperando...'
case 'confirmed':
return '¡Confirmado!'
case 'failed':
return 'Falló.'
}
}Type guards: refinando tipos
Cuando un valor puede ser de varios tipos, los type guards ayudan a TypeScript a entender cuál tipo es en 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 aquí
console.log(result.data.name)
} else {
// TS sabe que result.error existe aquí
console.error(result.error)
}
}
// Type guard personalizado
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 es User aquí
console.log(data.email)
}Configuración: tsconfig.json
El tsconfig.json define cómo TypeScript se comporta en el proyecto. Para proyectos modernos con Next.js o Node.js, esta es la base que recomiendo:
{
"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"]
}Lo más importante es "strict": true — activa todas las verificaciones de tipo. Proyectos sin strict mode pierden la mayor parte del valor de TypeScript.
Patrones que uso en producción
Después de usar TypeScript en todos los proyectos, estos son los patrones que más aplico día a día:
- **Nunca usar
any** — usaunknowncuando no sabes el tipo y haz type guard para validar - Inferencia primero — deja que TS infiera el tipo siempre que sea posible, tipar explícitamente solo cuando es necesario
- Interfaces para props de componentes — toda prop de componente React debe tener una interface
- Utility types para APIs —
Omit<User, 'id'>para creación,Partial<User>para updates - Union types en lugar de enums — más simples, sin overhead de runtime
- **
as const** — para arrays y objetos literales que no deben cambiar
// as const — convierte valores en tipos literales
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>
)
}Conclusión
TypeScript no se trata de escribir más código — se trata de escribir código que el compilador pueda validar. Los tipos documentan la intención, las interfaces definen contratos entre módulos, y los generics permiten reutilización sin perder seguridad. Si vienes de JavaScript, la curva de aprendizaje es suave: empieza tipando funciones y props, después avanza a generics y utility types. La ganancia en productividad y confianza en el código aparece rápido.
