¿Qué es TypeScript?
TypeScript es un superset de JavaScript con tipado estático que añade anotaciones de tipo opcionales que se verifican en tiempo de compilación. Se compila a JavaScript puro, por lo que se ejecuta en cualquier lugar donde JavaScript funcione: navegadores, Node.js, Deno y Bun.
Creado por Microsoft en 2012, TypeScript se ha convertido en la opción predeterminada para proyectos serios de JavaScript. La razón es sencilla: detecta errores antes de que el código se ejecute. Una errata en el nombre de una propiedad, un argumento faltante, un tipo de retorno incorrecto; TypeScript señala estos problemas en tiempo de compilación en lugar de permitir que se conviertan en caídas en producción.
La mejor manera de pensar en TypeScript es como JavaScript con una red de seguridad. Escribes el mismo lenguaje que ya conoces, pero puedes describir la forma de tus datos y los contratos de tus funciones. El compilador verifica que todo encaje y luego elimina los tipos, dejando un JavaScript limpio.
Por qué TypeScript es importante
JavaScript tiene un tipado dinámico, lo que significa que puedes pasar cualquier valor a cualquier función y el error solo aparecerá cuando ese código se ejecute. En un script pequeño esto no es problema, pero en una aplicación grande con decenas de desarrolladores, es una receta para generar bugs en producción.
TypeScript soluciona esto comprobando los tipos antes de que el código se ejecute:
- Detecta bugs en tiempo de compilación — si escribes mal una propiedad, pasas un tipo de argumento incorrecto o olvidas manejar un valor null, TypeScript te lo indica inmediatamente.
- Mejor experiencia en el editor — el autocompletado funciona porque el editor sabe qué propiedades tiene un objeto. El refactorizado es seguro porque el compilador encuentra cada lugar que necesita actualizarse.
- Código autodocumentado — las firmas de las funciones describen exactamente qué esperan y qué devuelven. Leer los tipos te indica cómo usar el código sin necesidad de leer la implementación.
- Refactorizado más seguro — cambia un tipo y el compilador resaltará cada lugar que se haya roto. Se acabó el buscar errores de ejecución por todo el código base.
- Adopción progresiva — puedes añadir TypeScript a un proyecto de JavaScript existente de forma incremental, archivo por archivo.
TypeScript no añade sobrecarga (overhead) en tiempo de ejecución. Los tipos se eliminan durante la compilación, por lo que el JavaScript que llega a los usuarios es idéntico, independientemente de si lo escribiste en TypeScript o no.
Cómo funciona TypeScript
El código de TypeScript reside en archivos .ts. El compilador de TypeScript (tsc) lee estos archivos, verifica los tipos y genera archivos .js con las anotaciones de tipo eliminadas.
// app.ts
function add(a: number, b: number): number {
return a + b;
}
const result = add(1, 2); // result is number
El compilador verifica que a y b sean números, que la función devuelva un número y que result reciba un número. Si algo no coincide, obtendrás un error en tiempo de compilación. El JavaScript resultante es simplemente:
function add(a, b) {
return a + b;
}
const result = add(1, 2);
Sin tipos, sin sobrecarga. La seguridad estuvo presente únicamente en el paso de compilación.
Anotaciones de tipo
Las anotaciones de tipo describen el tipo de una variable, parámetro o valor de retorno:
// primitives
let name: string = "Ada";
let age: number = 36;
let active: boolean = true;
// arrays
let skills: string[] = ["HTML", "CSS"];
let scores: Array<number> = [95, 87, 92];
// function parameters and return
function greet(name: string): string {
return `Hello, ${name}!`;
}
No siempre son necesarias las anotaciones. TypeScript infiere los tipos a partir del valor:
let count = 0; // inferred as number
const items = ["a"]; // inferred as string[]
Prefiere la inferencia cuando el tipo sea obvio. Utiliza anotaciones cuando el tipo no quede claro a partir del valor, cuando necesites un tipo más general o cuando la inferencia predeterminada sea demasiado restrictiva.
Inferencia de tipos
TypeScript deduce automáticamente los tipos a partir del contexto:
// from assignment
let x = 5; // number
// from return value
function doubling(n: number) {
return n * 2; // inferred as number
}
// from context
const numbers = [1, 2, 3]; // number[]
const doubled = numbers.map(n => n * 2); // number[]
La inferencia significa que escribes menos código manteniendo la misma seguridad. La mayor parte del código en TypeScript depende en gran medida de la inferencia; anota los tipos únicamente cuando aporte claridad.
Union types
Un union type indica que un valor puede ser de uno de varios tipos:
type Status = "loading" | "success" | "error";
function handleStatus(status: Status) {
if (status === "loading") {
// TypeScript knows status is "loading" here
}
}
let value: string | number;
value = "hello";
value = 42;
Los unions modelan muy bien los datos del mundo real. Las respuestas de una API pueden tener éxito o fallar. Un campo puede ser un string o null. Una función puede aceptar diferentes tipos de entrada.
Type narrowing
TypeScript realiza el estrechamiento de tipos (type narrowing) de los union types dentro de los condicionales:
function process(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
return value.toUpperCase();
}
// TypeScript knows value is number here
return value.toFixed(2);
}
El narrowing también funciona con:
- El operador
in— para comprobar si existe una propiedad instanceof— para comprobar instancias de clases- Comprobaciones de igualdad —
===y!== - Discriminated unions — comprobando una propiedad de etiqueta (tag) común
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;
function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
}
}
Las discriminated unions son uno de los patrones más potentes de TypeScript. La propiedad kind le indica a TypeScript exactamente con qué variante estás trabajando.
Interfaces y type aliases
Una interface describe la forma de un objeto: sus propiedades, sus tipos y si son obligatorias. Un type alias asigna un nombre a cualquier tipo, incluyendo uniones, tuplas, primitivos y tipos de función.
// Interface
interface User {
id: number;
name: string;
email: string;
}
// Type alias with the same shape
type User = {
id: number;
name: string;
email: string;
};
Para formas de objetos simples, ambos son intercambiables. Las diferencias aparecen cuando compones o extiendes tipos.
Propiedades opcionales y readonly
interface User {
id: number;
name: string;
email?: string; // optional — string | undefined
readonly createdAt: Date;
}
const user: User = { id: 1, name: "Ada", createdAt: new Date() };
user.createdAt = new Date(); // Error: cannot assign to a readonly property
Extender interfaces
Las interfaces heredan de otras interfaces mediante extends, pudiendo incluir varias a la vez:
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface User extends Timestamped {
id: number;
name: string;
}
Type aliases para uniones, tuplas y funciones
Los type aliases destacan cuando el tipo no es un objeto simple:
type ID = string | number;
type Status = "idle" | "loading" | "success" | "error";
type Pair = [string, number];
type Callback = (data: unknown) => void;
Tipos de intersección
Combina tipos con &:
type User = { id: number; name: string };
type WithTimestamp = { createdAt: Date };
type TimestampedUser = User & WithTimestamp;
¿Interface o type?
Usa interfaces para formas de objetos que puedan ser extendidas o implementadas por clases. Usa type aliases para uniones, intersecciones, tuplas y tipos de función. Elige una convención para las formas de objetos y mantenla de manera consistente.
Genéricos
Los genéricos son parámetros de tipo: permiten que una función, interfaz o clase funcione con cualquier tipo manteniendo la seguridad de tipos. En lugar de definir un tipo fijo o recurrir a any, pasas una variable de tipo que se completa en el momento de la llamada.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const num = first([1, 2, 3]); // number
const str = first(["a", "b"]); // string
Sin genéricos, tendrías que usar any y perder la información del tipo, o escribir una función independiente para cada tipo.
Funciones genéricas
Un parámetro de tipo se declara entre paréntesis angulares. TypeScript generalmente lo infiere a partir de los argumentos, por lo que rara vez es necesario pasarlo explícitamente:
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
const result = merge({ name: "Ada" }, { age: 36 });
// { name: string; age: number }
Interfaces y clases genéricas
interface Response<T> {
data: T;
status: number;
}
interface User {
id: number;
name: string;
}
const response: Response<User> = {
data: { id: 1, name: "Ada" },
status: 200,
};
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
Restricciones (Constraints)
Usa extends para restringir lo que un genérico puede aceptar:
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
Parámetros de tipo por defecto
interface Paginated<T = unknown> {
data: T[];
total: number;
page: number;
}
const response: Paginated = { data: [1, "a"], total: 10, page: 1 };
Los genéricos se eliminan en tiempo de ejecución, por lo que no añaden sobrecarga; existen únicamente para que el compilador y tu editor sean más inteligentes.
El archivo tsconfig.json
El archivo tsconfig.json configura el compilador de TypeScript:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Opciones clave:
- target — qué versión de JavaScript generar (ES2020 es un buen valor predeterminado)
- strict — habilita todos los flags de comprobación de tipos estrictos
- module — el sistema de módulos para la salida (ESNext para proyectos modernos)
- outDir — dónde se guarda el JavaScript compilado
- esModuleInterop — permite importaciones por defecto desde módulos CommonJS
Usa siempre "strict": true para proyectos nuevos. Detecta más errores a cambio de requerir un código ligeramente más explícito.
Instalando TypeScript
# As a project dependency (recommended)
npm install --save-dev typescript
# Globally (not recommended for teams)
npm install -g typescript
Compila tu proyecto:
# Using npx
npx tsc
# Or add a script to package.json
npm run build
Para desarrollo con modo watch:
npx tsc --watch
La mayoría de los frameworks modernos (Next.js, Vite, Astro) gestionan la compilación de TypeScript por ti; no es necesario ejecutar tsc manualmente.
Errores comunes
- Usar
anyen todas partes, lo que desactiva la comprobación de tipos. - Anotar todo cuando la inferencia ya conoce el tipo.
- No activar el modo estricto en el tsconfig.
- Confundir los tipos de TypeScript con valores en tiempo de ejecución; los tipos se eliminan en tiempo de compilación.
- Importar tipos incorrectamente: utiliza
import typepara importaciones exclusivas de tipos. - Ignorar los errores del compilador en lugar de solucionarlos.
- No instalar los paquetes
@typespara las librerías de JavaScript. - Hacer que todas las propiedades de una interfaz sean opcionales, lo que oculta las comprobaciones obligatorias.
- Usar
anyen lugar de un genérico, descartando la información de tipo que querías conservar.
Mejores prácticas
- Activa el modo estricto desde el primer día.
- Deja que la inferencia haga el trabajo; anota los tipos solo cuando aporte valor.
- Usa
unknownen lugar deanycuando no conozcas el tipo. - Prefiere las interfaces para formas de objetos que puedan extenderse.
- Usa uniones discriminadas para máquinas de estado y datos variantes.
- Mantén las definiciones de tipos cerca de donde se utilizan.
- Usa
import typepara importaciones solo de tipos y así mantener el código de ejecución ligero. - Trata al compilador como a un compañero de equipo, no como un obstáculo.
Qué aprender a continuación
Ya dominas los fundamentos de TypeScript: anotaciones, inferencia, uniones, narrowing, interfaces, type aliases y genéricos. A partir de aquí, el siguiente paso natural son los Advanced Types para aprender sobre utility types, conditional types y mapped types. Elige un proyecto, refactoriza algo de código y deja que la práctica haga el resto.