En esta página
Clases y decoradores
Clases en TypeScript
TypeScript extiende las clases de JavaScript con un sistema de tipos completo: modificadores de acceso, propiedades estrictas, clases abstractas y chequeo de implementaciones de interfaces. Todo lo que aprendas aquí compila a JavaScript estándar.
Modificadores de acceso
TypeScript ofrece cuatro niveles de visibilidad:
| Modificador | Accesible desde |
|---|---|
public (por defecto) |
Cualquier lugar |
private |
Solo la clase donde se declara |
protected |
La clase y sus subclases |
readonly |
Solo lectura (combinable con los anteriores) |
class CuentaBancaria {
public titular: string;
private saldo: number;
protected numeroCuenta: string;
readonly fechaCreacion: Date;
constructor(titular: string, saldoInicial: number) {
this.titular = titular;
this.saldo = saldoInicial;
this.numeroCuenta = Math.random().toString(36).slice(2).toUpperCase();
this.fechaCreacion = new Date();
}
public depositar(cantidad: number): void {
if (cantidad <= 0) throw new Error("La cantidad debe ser positiva");
this.saldo += cantidad;
}
public retirar(cantidad: number): boolean {
if (cantidad > this.saldo) return false;
this.saldo -= cantidad;
return true;
}
public consultarSaldo(): number {
return this.saldo;
}
}
const cuenta = new CuentaBancaria("Ana García", 1000);
cuenta.depositar(500);
console.log(cuenta.consultarSaldo()); // 1500
// cuenta.saldo; // ❌ Error: propiedad privada
// cuenta.fechaCreacion = new Date(); // ❌ Error: solo lectura`private` vs `#private` (nativo de JS)
TypeScript también soporta los campos privados nativos de JavaScript (ECMAScript) con el prefijo #. A diferencia del private de TypeScript, estos son realmente privados en tiempo de ejecución:
class Token {
#secreto: string; // Privado en tiempo de ejecución
constructor(secreto: string) {
this.#secreto = secreto;
}
verificar(intento: string): boolean {
return this.#secreto === intento;
}
}Propiedades de parámetros
Una de las características más cómodas de TypeScript es poder declarar y asignar propiedades directamente en los parámetros del constructor:
// Sin propiedades de parámetros (verbose):
class ProductoVerboso {
public readonly nombre: string;
private precio: number;
protected categoria: string;
constructor(nombre: string, precio: number, categoria: string) {
this.nombre = nombre;
this.precio = precio;
this.categoria = categoria;
}
}
// Con propiedades de parámetros (conciso, equivalente):
class Producto {
constructor(
public readonly nombre: string,
private precio: number,
protected categoria: string
) {}
obtenerPrecio(): number {
return this.precio;
}
}Miembros estáticos
Los miembros static pertenecen a la clase, no a las instancias:
class Contador {
private static instancias = 0;
private id: number;
constructor(public readonly nombre: string) {
Contador.instancias++;
this.id = Contador.instancias;
}
static obtenerTotal(): number {
return Contador.instancias;
}
toString(): string {
return `Contador #${this.id} (${this.nombre})`;
}
}
const c1 = new Contador("Primero");
const c2 = new Contador("Segundo");
console.log(Contador.obtenerTotal()); // 2Clases abstractas
Una clase abstracta define una plantilla: puede tener implementaciones concretas y también métodos abstractos que cada subclase debe implementar:
abstract class Exportador {
// Método concreto reutilizable
protected formatearFecha(fecha: Date): string {
return fecha.toISOString().split("T")[0];
}
// Método abstracto: cada subclase elige cómo exportar
abstract exportar(datos: unknown[]): string;
// Template method: usa el método abstracto
exportarConEncabezado(datos: unknown[], titulo: string): string {
const contenido = this.exportar(datos);
return `=== ${titulo} ===\n${contenido}`;
}
}
class ExportadorCSV extends Exportador {
exportar(datos: unknown[]): string {
return datos.map((fila) => JSON.stringify(fila)).join("\n");
}
}
class ExportadorJSON extends Exportador {
exportar(datos: unknown[]): string {
return JSON.stringify(datos, null, 2);
}
}
// const exp = new Exportador(); // ❌ No se puede instanciar clase abstracta
const csv = new ExportadorCSV();
const json = new ExportadorJSON();
const filas = [{ id: 1, nombre: "Ana" }, { id: 2, nombre: "Luis" }];
console.log(csv.exportarConEncabezado(filas, "Usuarios"));
console.log(json.exportarConEncabezado(filas, "Usuarios"));Implementación de interfaces con `implements`
implements fuerza a una clase a cumplir con un contrato de interfaz:
interface Serializable {
serializar(): string;
deserializar(datos: string): void;
}
interface Validable {
esValido(): boolean;
obtenerErrores(): string[];
}
class FormularioPedido implements Serializable, Validable {
constructor(
public productoId: string,
public cantidad: number,
public direccion: string
) {}
serializar(): string {
return JSON.stringify({ productoId: this.productoId, cantidad: this.cantidad, direccion: this.direccion });
}
deserializar(datos: string): void {
const obj = JSON.parse(datos) as { productoId: string; cantidad: number; direccion: string };
this.productoId = obj.productoId;
this.cantidad = obj.cantidad;
this.direccion = obj.direccion;
}
esValido(): boolean {
return this.obtenerErrores().length === 0;
}
obtenerErrores(): string[] {
const errores: string[] = [];
if (!this.productoId) errores.push("El producto es obligatorio");
if (this.cantidad <= 0) errores.push("La cantidad debe ser mayor que cero");
if (!this.direccion.trim()) errores.push("La dirección no puede estar vacía");
return errores;
}
}Decoradores TC39 (TypeScript 5+)
Los decoradores son funciones que modifican clases, métodos, campos o accesorios de forma declarativa. Los decoradores TC39 (stage 3) son los modernos, soportados desde TypeScript 5.0 sin ningún flag de configuración.
Anatomía de un decorador de método
function decoradorMetodo(
target: unknown,
context: ClassMethodDecoratorContext
) {
// target: la función del método original
// context: información sobre el método (nombre, tipo, etc.)
context.addInitializer(function (this: unknown) {
// Se ejecuta durante la inicialización de la instancia
// `this` es la instancia de la clase
});
}Decorador de logging
function log(
target: unknown,
context: ClassMethodDecoratorContext
): void {
const nombreMetodo = String(context.name);
context.addInitializer(function (this: Record<string, unknown>) {
const original = this[nombreMetodo] as (...args: unknown[]) => unknown;
this[nombreMetodo] = function (...args: unknown[]) {
const inicio = performance.now();
console.log(`▶ ${nombreMetodo}(${args.map((a) => JSON.stringify(a)).join(", ")})`);
const resultado = original.apply(this, args);
const duracion = (performance.now() - inicio).toFixed(2);
console.log(`◀ ${nombreMetodo} → ${JSON.stringify(resultado)} (${duracion}ms)`);
return resultado;
};
});
}Decorador de clase
function singleton<T extends new (...args: unknown[]) => unknown>(
target: T,
_context: ClassDecoratorContext
): T {
let instancia: InstanceType<T> | null = null;
return class extends target {
constructor(...args: unknown[]) {
if (instancia) return instancia as InstanceType<T>;
super(...args);
instancia = this as unknown as InstanceType<T>;
}
} as T;
}Ejemplo completo: servicio con logging y singleton
// ──────────────────────────────────────────────────────────────
// Decorador de logging para métodos
// ──────────────────────────────────────────────────────────────
function log(target: unknown, context: ClassMethodDecoratorContext): void {
const nombre = String(context.name);
context.addInitializer(function (this: Record<string, unknown>) {
const original = this[nombre] as (...args: unknown[]) => unknown;
this[nombre] = function (...args: unknown[]) {
console.log(`[LOG] ${nombre} iniciado`);
try {
const resultado = original.apply(this, args);
console.log(`[LOG] ${nombre} completado correctamente`);
return resultado;
} catch (error) {
console.error(`[LOG] ${nombre} falló:`, error);
throw error;
}
};
});
}
// ──────────────────────────────────────────────────────────────
// Interfaces del dominio
// ──────────────────────────────────────────────────────────────
interface Usuario {
id: string;
nombre: string;
email: string;
rol: "admin" | "usuario";
}
interface RepositorioUsuarios {
buscarPorId(id: string): Usuario | null;
buscarTodos(): Usuario[];
guardar(usuario: Omit<Usuario, "id">): Usuario;
}
// ──────────────────────────────────────────────────────────────
// Clase de servicio con decoradores
// ──────────────────────────────────────────────────────────────
class ServicioUsuarios {
private readonly repo: RepositorioUsuarios;
private cache: Map<string, Usuario> = new Map();
constructor(repo: RepositorioUsuarios) {
this.repo = repo;
}
@log
obtenerUsuario(id: string): Usuario | null {
if (this.cache.has(id)) {
console.log("[CACHE] Hit para", id);
return this.cache.get(id)!;
}
const usuario = this.repo.buscarPorId(id);
if (usuario) this.cache.set(id, usuario);
return usuario;
}
@log
crearUsuario(datos: Omit<Usuario, "id">): Usuario {
const nuevo = this.repo.guardar(datos);
this.cache.set(nuevo.id, nuevo);
return nuevo;
}
@log
obtenerAdmins(): Usuario[] {
return this.repo.buscarTodos().filter((u) => u.rol === "admin");
}
}
// ──────────────────────────────────────────────────────────────
// Repositorio en memoria (implementación de prueba)
// ──────────────────────────────────────────────────────────────
class RepositorioEnMemoria implements RepositorioUsuarios {
private usuarios: Usuario[] = [
{ id: "u1", nombre: "Ana García", email: "[email protected]", rol: "admin" },
{ id: "u2", nombre: "Luis Pérez", email: "[email protected]", rol: "usuario" },
];
buscarPorId(id: string): Usuario | null {
return this.usuarios.find((u) => u.id === id) ?? null;
}
buscarTodos(): Usuario[] {
return [...this.usuarios];
}
guardar(datos: Omit<Usuario, "id">): Usuario {
const nuevo: Usuario = { ...datos, id: `u${this.usuarios.length + 1}` };
this.usuarios.push(nuevo);
return nuevo;
}
}
// ──────────────────────────────────────────────────────────────
// Uso
// ──────────────────────────────────────────────────────────────
const repo = new RepositorioEnMemoria();
const servicio = new ServicioUsuarios(repo);
const ana = servicio.obtenerUsuario("u1");
const luis = servicio.obtenerUsuario("u2");
const ana2 = servicio.obtenerUsuario("u1"); // Desde caché
console.log(ana?.nombre); // Ana García
console.log(luis?.nombre); // Luis Pérez
console.log(ana2?.nombre); // Ana García (desde caché)
const carlos = servicio.crearUsuario({
nombre: "Carlos Ríos",
email: "[email protected]",
rol: "usuario",
});
console.log("Creado:", carlos.nombre);
console.log("Admins:", servicio.obtenerAdmins().map((u) => u.nombre));Resumen
- Los modificadores de acceso (
public,private,protected,readonly) comunican intención y el compilador los verifica. - Las propiedades de parámetros eliminan el boilerplate repetitivo en constructores.
- Las clases abstractas definen contratos con implementación parcial; excelentes para el patrón Template Method.
implementsgarantiza que la clase cumple uno o más contratos de interfaz.- Los decoradores TC39 permiten extender comportamiento de forma declarativa y reutilizable sin modificar las clases destino.
En la siguiente lección aprenderás cómo organizar tu código TypeScript en módulos y namespaces, incluyendo importaciones type-only, resolución de módulos y archivos de declaración.
Inicia sesión para guardar tu progreso