🟦 Fundamentos de TypeScript
Qué es TypeScript y por qué usarlo, tsconfig.json, tipos primitivos, funciones, objetos, interfaces, clases, modules, uniones y narrowing con ejemplos reales.
Fundamentos de TypeScript
TypeScript es JavaScript con tipos que se compila a JavaScript puro. Nació en Microsoft (2012) y hoy es el estándar de facto en el backend y el frontend porque mueve errores de la ejecución a la compilación: el editor te avisa de un bug antes de que el servidor lo descubra en producción. No es un lenguaje nuevo: es un superset. Todo lo que sabes de JS sigue funcionando; TS añade una capa que se borra al compilar.
Qué es TypeScript y por qué
Un programa TypeScript pasa por una compilación (tsc) que hace dos cosas: comprueba tipos y elimina las anotaciones, produciendo JavaScript puro que Node o el navegador ejecutan igual que siempre.
Beneficios concretos: autocompletado real (no escribes propiedades a ciegas), contratos de funciones (no pasas argumentos del tipo equivocado), refactor seguro (renombrar algo no rompe en silencio) y documentación viva (el tipo ES la documentación de la API).
💡 El tipo se comprueba en tiempo de compilación, no en ejecución. Cuando la app corre, las anotaciones ya no existen. Es por eso que a TS se le llama estático (las comprobaciones ocurren antes de ejecutar).
Tu primer archivo y tsconfig.json
Un proyecto TS se configura con un solo archivo: tsconfig.json, la fuente de verdad de cómo compila tu código.
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
Las opciones que importan de verdad:
| Opción | Qué controla |
|---|---|
target |
Versión de JS que se emite (ES2022 = async/await nativo, ES5 = polyfills de Promise) |
module |
Sistema de módulos de salida: nodenext, commonjs, esnext |
strict |
Activa todas las comprobaciones estrictas. Actívalo siempre |
outDir |
Carpeta de salida del JS compilado |
rootDir |
Carpeta raíz del código fuente |
esModuleInterop |
Permite import express from "express" aunque la librería use module.exports |
# inicializar un proyecto
npm init -y
npm install -D typescript
npx tsc --init # genera un tsconfig.json comentado
npx tsc # compila según tsconfig
npx tsc --watch # recompila al guardar
⚠️
strictno es opcional si quieres lo que TS promete. Constrict: falseel compilador ignoranullyundefined, y pierdes exactamente la categoría de bugs que viniste a eliminar.
Tipos primitivos y anotaciones
TS detecta tipos automáticamente (inferencia), pero puedes anotarlos de forma explícita:
const nombre: string = "Ana";
const edad: number = 30;
const activo: boolean = true;
// TS puede inferir sin anotar:
let ciudad = "Madrid"; // tipo string (inferido)
// ciudad = 42; // ❌ Error: no se puede asignar number a string
// null y undefined son tipos por derecho propio
let dato: string | null = null;
Los primitivos de TS son los mismos de JS: string, number, boolean, bigint, symbol, null, undefined. Lo importante no es memorizar la lista sino la regla: una vez que un valor tiene tipo, TS vigila que no lo violes.
💡 Diferencia clave:
string(tipo) vsString(objeto). Siempre usa minúsculas para los tipos; las mayúsculas son los constructores de JS y rara vez quieres usarlos como tipos.
Arrays y tuplas
// Arrays
const numeros: number[] = [1, 2, 3];
const nombres: Array<string> = ["Ana", "Luis"]; // notación genérica equivalente
numeros.push(4); // ✅
// numeros.push("x"); // ❌ Error: string no es asignable a number
// Tuplas: arrays con longitud y posición fijas
const punto: [number, number] = [10, 20];
// punto = [10]; // ❌ Error: falta el segundo elemento
⚠️ Las tuplas usan el tipo de un array, así que TS no siempre bloquea métodos mutadores como
push. Para datos que nunca deben cambiar, decláralosreadonly [number, number].
Funciones: parámetros, retorno y valores especiales
// Anotar parámetros y retorno
function saludar(nombre: string): string {
return `Hola, ${nombre}`;
}
// Parámetros opcionales (?) y con valor por defecto (=)
function configurar(host: string, puerto?: number, ssl: boolean = true) {
return { host, puerto: puerto ?? 443, ssl }; // puerto → 443 si falta
}
// Rest parameters: número variable de argumentos
function sumar(...nums: number[]): number {
return nums.reduce((acc, n) => acc + n, 0);
}
sumar(1, 2, 3, 4); // 10
// Arrow functions tipadas y retorno void (no devuelve nada útil)
const doble = (x: number): number => x * 2;
function log(msg: string): void { console.log(msg); }
Objetos y tipos literales
// Anotar un objeto inline
function imprimir(coordenadas: { x: number; y: number }) {
console.log(coordenadas.x, coordenadas.y);
}
imprimir({ x: 10, y: 20 }); // ✅
// imprimir({ x: 10 }); // ❌ falta y
// Propiedades opcionales y readonly
type Config = { readonly id: string; host: string; cache?: number };
Los objetos se describen con su forma: { campo: tipo }. No importa el nombre de la “clase” de origen, solo su estructura. Eso es lo que hace estructural al sistema de tipos.
Interfaces
Una interface nombra una forma de objeto para reutilizarla y extenderla:
interface Usuario {
id: number;
nombre: string;
email?: string; // opcional
readonly createdAt: Date; // solo lectura
}
interface Admin extends Usuario {
permisos: string[]; // añade campos
revocar(): void; // y métodos
}
const ana: Admin = {
id: 1,
nombre: "Ana",
createdAt: new Date(),
permisos: ["leer", "escribir"],
revocar() { this.permisos = []; },
};
// ana.createdAt = new Date(); // ❌ readonly
| Palabra clave | Efecto |
|---|---|
extends |
Hereda los campos de otra interface (y varias: A extends B, C) |
? |
Propiedad opcional |
readonly |
Propiedad de solo lectura: no se puede reasignar |
Clases
Las clases de TS son las de ES2015 con modificadores de acceso y soporte para implements:
class CuentaBancaria {
public titular: string; // accesible desde fuera
protected numero: string; // accesible en esta clase y subclases
private saldo: number; // solo dentro de la clase
readonly moneda: string = "EUR"; // no se puede reasignar
constructor(titular: string, numero: string, saldoInicial: number) {
this.titular = titular;
this.numero = numero;
this.saldo = saldoInicial;
}
depositar(monto: number): void {
if (monto <= 0) throw new Error("Monto inválido");
this.saldo += monto;
}
obtenerSaldo(): number { return this.saldo; }
}
const cuenta = new CuentaBancaria("Ana", "ES-0001", 100);
cuenta.depositar(50);
// cuenta.saldo; // ❌ Error: private
// cuenta.numero; // ❌ Error: protected
💡 Atajo de TS: si declaras los parámetros del constructor con modificador (
constructor(private saldo: number)), TS crea la propiedad, la tipa y la asigna automáticamente. Menos código, mismo resultado.
Las clases también pueden implementar una interface: el contrato te lo da la interface, la implementación la clase:
interface Repositorio {
guardar(dato: string): void;
listar(): string[];
}
class RepositorioEnMemoria implements Repositorio {
private datos: string[] = [];
guardar(dato: string) { this.datos.push(dato); }
listar() { return this.datos; }
}
type alias vs interface
Ambos definen formas de objeto. Elige por capacidad:
| Capacidad | type |
interface |
|---|---|---|
| Uniones e intersecciones | ✅ | ❌ |
| Tuplas | ✅ | ❌ |
| Mapped / conditional types | ✅ | ❌ |
| Extender | & |
extends |
| Declaración merging (reabrir) | ❌ | ✅ |
type Id = string | number; // union: imposible con interface
interface Animal { nombre: string }
type Perro = Animal & { ladra(): void }; // intersección con &
💡 Regla práctica de la comunidad:
interfacepara objetos públicos y clases;typepara uniones, tuplas y tipos derivados. Para el 90% de los casos son intercambiables.
Uniones básicas
Una union (|) dice que el valor puede ser de uno de varios tipos:
type Resultado = "exito" | "error";
type Id = string | number;
function buscar(id: Id) {
// No puedes usar el valor todavía: no sabes cuál de los dos es.
// Hace falta narrowing (abajo) para operar con él.
if (typeof id === "string") {
return id.toUpperCase();
}
return id.toFixed(0);
}
function mostrar(resultado: Resultado) {
console.log(resultado === "exito" ? "Todo ok" : "Algo falló");
}
Las uniones también combinadas con objetos (uniones discriminadas) son la herramienta para modelar estados:
type Pedido =
| { estado: "creado"; id: number }
| { estado: "pagado"; id: number; total: number }
| { estado: "cancelado"; id: number; motivo: string };
null, undefined y strictNullChecks
Con strict: true (que activa strictNullChecks), null y undefined no son asignables a otros tipos:
// Con strictNullChecks activo:
let nombre: string = null; // ❌ Error: null no es asignable a string
let ciudad: string | null = null; // ✅ union explícita
let email: string | undefined; // ✅ (variable sin inicializar)
Esta es la mayor fuente de crashes en JS (“no puede leer propiedad de undefined”). TS te obliga a manejar el caso nulo de forma explícita:
function obtenerCliente(): { nombre: string } | null {
return null; // o un cliente, según el caso
}
const c = obtenerCliente();
// c.nombre; // ❌ Error: c puede ser null
if (c !== null) {
console.log(c.nombre); // ✅ dentro del if, c es Cliente
}
⚠️ El operador
!(non-null assertion) le dice a TS “confía, esto no es null”:c!.nombre. Es un escape hatch. Úsalo con moderación: si te equivocas, el crash vuelve en runtime.
never, unknown y void
Tres tipos que suelen confundirse:
| Tipo | Qué significa | Cuándo usarlo |
|---|---|---|
void |
No devuelve valor útil | Funciones que solo ejecutan efectos (logs, eventos) |
unknown |
Hay un valor, pero no sé su tipo | Datos externos: JSON.parse, respuestas de API |
never |
El código nunca llega aquí | Funciones que lanzan, loops infinitos, exhaustividad |
function lanzar(): never {
throw new Error("Siempre falla"); // nunca devuelve control
}
// unknown: debes reducirlo antes de usarlo
function procesar(valor: unknown) {
if (typeof valor === "string") {
return valor.toUpperCase(); // ✅ reducido a string
}
// valor.length; // ❌ Error: unknown no tiene length
}
💡
unknownes elanyseguro: cualquier cosa puede serunknown, pero no puedes usarlo sin narrowing. El compilador sigue protegiéndote.anydesactiva el compilador por completo.
Narrowing básico: typeof y truthiness
Narrowing es convertir un tipo amplio en uno concreto dentro de un bloque de código:
function calcular(valor: string | number | null) {
// 1) Truthiness: descarta null y undefined
if (valor == null) return 0;
// 2) typeof: distingue primitivos
if (typeof valor === "string") {
return valor.length; // valor es string aquí
}
return valor.toFixed(2); // aquí es number
}
⚠️ Cuidado con la truthiness en valores “falsy” válidos:
if (contador)falla cuandocontador === 0. Para números usa!= nully compara con el valor real (if (contador > 0)).
Modules: export / import
TypeScript usa los módulos de ES para organizar código. Cada archivo con export/import es un módulo con su propio ámbito.
// math.ts
export const PI = 3.14159;
export function area(radio: number): number {
return PI * radio * radio;
}
export default class Util { /* ... */ }
// app.ts
import { PI, area } from "./math.js";
import Util from "./math.js";
console.log(area(2)); // ✅ usa la función importada
export default exporta un único valor principal por módulo; export type / import type exportan solo tipos (se borran al compilar).
ESM vs CommonJS
JS tiene dos sistemas de módulos; TS los soporta y es clave saber cuál estás usando:
| CommonJS (CJS) | ES Modules (ESM) | |
|---|---|---|
| Sintaxis | require() / module.exports |
import / export |
| Carga | Síncrona, en tiempo de ejecución | Estática, analizable por el motor |
| Mismo archivo | .js en paquete sin "type": "module" |
.js con "type": "module", .mjs |
import de CJS |
✅ permitido | — |
En el tsconfig, module debe reflejar el entorno: "module": "nodenext" respeta el campo "type" de package.json. Un archivo CJS se escribe con require, uno ESM con import.
Ejemplos reales completos
// 1) Manejo de resultado con union discriminada y exhaustividad
type OperacionResultado<T> =
| { ok: true; data: T }
| { ok: false; error: string };
function ejecutar(): OperacionResultado<string[]> {
try {
return { ok: true, data: ["a", "b"] };
} catch (e) {
return { ok: false, error: (e as Error).message };
}
}
const res = ejecutar();
if (res.ok) {
console.log(res.data.length); // ✅ narrowed a string[]
} else {
console.error(res.error); // ✅ narrowed a { error: string }
}
Cheatsheet
| Quieres… | Usas… |
|---|---|
| “Configurar el compilador” | tsconfig.json con strict: true |
| “Tipar un parámetro” | function f(x: string) |
| “Retorno con forma” | interface o type de objeto |
| “Varios tipos posibles” | union string | number |
| “Campo opcional” | campo?: tipo |
| “Campo inmutable” | readonly campo |
| “Acceso encapsulado” | private / protected en clases |
| “Dato externo inseguro” | unknown + narrowing con typeof |
| “Función que lanza” | never como retorno |
| “Reutilizar un tipo” | export type + import type |
Para profundizar
- TypeScript Handbook — The Basics: el punto de partida oficial sobre tipos y anotaciones.
- TypeScript Handbook — tsconfig Reference: documentación de todas las opciones del compilador, incluida
strict. - TypeScript Handbook — Everyday Types: primitivos, arrays, funciones, objetos y uniones en detalle.
- TypeScript Playground: experimenta con tipos y mira el JS emitido al instante.
- Ruta completa: TypeScript.