sgmonda@web:~$ cat blog/deja-de-usar-clases-en-javascript
====================================================================================================================================================================================================================================
Deja de usar clases en JavaScript
====================================================================================================================================================================================================================================

Si llevas tiempo trabajando con JavaScript o TypeScript, seguro que te has encontrado esto más veces de las que te gustaría:

export class MathHelper {
  static sum(a: number, b: number): number {
    return a + b;
  }

  static multiply(a: number, b: number): number {
    return a * b;
  }
}

Una clase sin constructor, sin estado, sin instancias. Todo static. Es un patrón tan extendido que mucha gente ni se plantea si tiene sentido. La respuesta corta es “no”. ¿Quieres saber por qué?

El problema semántico: las palabras importan

Cuando un desarrollador ve class, su cerebro activa un modelo mental concreto: objetos, estado, herencia, polimorfismo. Es el contrato implícito de la palabra class en cualquier lenguaje orientado a objetos. Escribir una clase que no usa nada de esto es como poner un cartel de “restaurante” en una gasolinera que solo vende café. Técnicamente sirve bebidas, pero el cartel confunde.

Este ruido semántico tiene un coste real. Cuando alguien nuevo llega al código y ve class StoreHelper, se pregunta: ¿se instancia? ¿Tiene estado? ¿Hay herencia en algún sitio? Tiene que leer toda la clase para descubrir que no, que es un puñado de funciones estáticas disfrazadas de objeto.

La herencia de Java: un problema que JavaScript no tiene

En Java, todo tiene que vivir dentro de una clase. Si quieres una función suelta, mala suerte — necesitas una clase contenedora. El patrón de la clase estática no es un diseño elegante, es una limitación del lenguaje. Es una muleta.

JavaScript no tiene esa limitación. Tiene un sistema de módulos real desde ES2015. Un archivo ya es un módulo. Las funciones pueden ser ciudadanas de primera clase sin necesitar una clase que les dé cobijo. Importar el patrón de la clase estática a JavaScript es como llevar un paraguas a un sitio donde no llueve porque “en tu ciudad siempre lo llevas”.

Cuando alguien escribe una clase puramente estática en TypeScript, no está usando una feature del lenguaje — está compensando una carencia de otro lenguaje que aquí no existe.

Las diferencias técnicas que sí importan

Más allá de la semántica, hay diferencias técnicas concretas que penalizan el uso de clases estáticas.

La privacidad es de mentira

export class ApiHelper {
  private static formatUrl(path: string): string {
    return `https://api.example.com${path}`;
  }

  public static get(path: string): Promise<Response> {
    return fetch(ApiHelper.formatUrl(path));
  }
}

Ese private solo existe en tiempo de compilación. TypeScript lo borra y el JavaScript resultante expone ApiHelper.formatUrl sin restricciones. Cualquiera que consuma tu código desde JavaScript puro puede llamarlo. Es privacidad decorativa.

Con módulos, la encapsulación es real:

function formatUrl(path: string): string {
  return `https://api.example.com${path}`;
}

export function get(path: string): Promise<Response> {
  return fetch(formatUrl(path));
}

formatUrl no se exporta. No existe fuera del módulo. No hay truco, cast ni escape posible. La privacidad la garantiza el runtime, no el compilador.

Tree-shaking: el código muerto que no se va

Los bundlers modernos (Webpack, Rollup, esbuild) pueden analizar qué exportaciones de un módulo se usan realmente y eliminar el resto. Es lo que se conoce como tree-shaking.

Con funciones exportadas individualmente, esto funciona de forma granular. Si solo importas get, el bundler elimina formatUrl del bundle final.

Con una clase estática, importas la clase entera. El bundler no puede saber qué métodos estáticos usas y cuáles no de forma fiable, así que se queda todo. En aplicaciones grandes, esto se traduce en kilobytes de código muerto que acaban en producción.

Verbosidad innecesaria

Compara:

// Clase estática
export class StringHelper {
  static capitalize(str: string): string {
    return str.charAt(0).toUpperCase() + str.slice(1);
  }

  static truncate(str: string, length: number): string {
    return str.length > length ? str.slice(0, length) + '...' : str;
  }
}
// Funciones + módulo
export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function truncate(str: string, length: number): string {
  return str.length > length ? str.slice(0, length) + '...' : str;
}

Menos indentación, menos ruido, misma funcionalidad. Y si quieres agrupar bajo un nombre, siempre puedes hacer import * as StringHelper from './string-helper' en el consumidor.

Testing más limpio

Las funciones individuales son más fáciles de mockear en tests. En lugar de parchear métodos de una clase que nunca se instancia, simplemente reemplazas la función importada. Herramientas como jest.mock trabajan de forma más natural con módulos que con métodos estáticos.

¿Cuándo sí tiene sentido una clase?

La regla es sencilla: si no usas lo que una clase te da, no uses una clase.

Una clase tiene sentido cuando hay estado de instancia (datos que pertenecen a cada objeto), cuando necesitas herencia o polimorfismo, cuando usas decoradores (Angular, NestJS), o cuando el patrón requiere instanciación (inyección de dependencias, por ejemplo).

Si tu clase no tiene constructor, no tiene propiedades de instancia y todos sus métodos son static, no es una clase. Es un módulo que no sabe que lo es.

El patrón correcto

// helpers/math.ts

function validate(n: number): void {
  if (!Number.isFinite(n)) throw new Error('Invalid number');
}

export function sum(a: number, b: number): number {
  validate(a);
  validate(b);
  return a + b;
}

export function multiply(a: number, b: number): number {
  validate(a);
  validate(b);
  return a * b;
}

validate es privada de verdad. sum y multiply son exportaciones con tree-shaking granular. No hay ruido semántico. No hay indirección innecesaria.

Y si el consumidor quiere un namespace explícito, tiene libertad para elegir:

import { sum, multiply } from './helpers/math';
import * as MathHelper from './helpers/math';

Conclusión

JavaScript tiene módulos. Úsalos. Las clases son herramientas excelentes cuando necesitas lo que ofrecen. Pero cuando todo es static, estás usando los alicates para clavar un clavo. Puedes, pero hay un martillo justo al lado.

No importa que lo hagas en Java. Java no tiene elección. Tú sí.