🔎 Buscar

🟦 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.

Wiki / Apuntes📖 Contenido

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

⚠️ strict no es opcional si quieres lo que TS promete. Con strict: false el compilador ignora null y undefined, 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) vs String (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áralos readonly [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: interface para objetos públicos y clases; type para 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
}

💡 unknown es el any seguro: cualquier cosa puede ser unknown, pero no puedes usarlo sin narrowing. El compilador sigue protegiéndote. any desactiva 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 cuando contador === 0. Para números usa != null y 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.