TypeScript: Clases, herencia y patrones de diseño

TypeScript: Clases, herencia y patrones de diseño

Tabla de Contenido

Note

Sexto artículo de la serie sobre TypeScript. Recorreremos las clases: herencia, modificadores de acceso, clases abstractas, super, getters/setters, miembros estáticos, tipado estructural y los patrones de diseño que habilitan.

Las clases en TypeScript parten de la sintaxis de JavaScript y la enriquecen con modificadores de acceso, clases abstractas y tipado estructural. El resultado es un modelo de orientación a objetos cercano al de lenguajes como C++ o Java, pero que compila a JavaScript estándar. Analicemos sus capacidades.

Clases y herencia

La declaración usa class y la herencia se establece con extends:

class Piece {}

class King extends Piece {}
class Queen extends Piece {}
class Bishop extends Piece {}

Podemos restringir los atributos a valores concretos combinando clases con tipos literales:

type Color = 'Black' | 'White'
type File = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H'
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8

class Position {
  constructor(
    private file: File,
    private rank: Rank
  ) {}
}

Declarar los parámetros del constructor con un modificador (private file: File) es un atajo: TypeScript crea y asigna la propiedad automáticamente, evitando el habitual this.file = file.

Modificadores de acceso

TypeScript define tres niveles de visibilidad para atributos y métodos:

  • public: accesible desde cualquier lugar. Es el nivel por defecto.
  • protected: accesible desde la propia clase y sus subclases.
  • private: accesible únicamente desde la propia clase.
class Avenger {
  constructor(
    private name: string,
    public team: string,
    public realName?: string
  ) {}

  public bio() {
    return `${this.name} ${this.team}`
  }

  private bioInterno() {
    return `${this.realName} ${this.team}`
  }
}

Warning

private y protected solo aplican en tiempo de compilación. El JavaScript emitido no impide el acceso en runtime; para privacidad real en ejecución usa los campos privados nativos de JavaScript (#campo).

Clases abstractas

Una clase abstract define una plantilla que no puede instanciarse, pero sirve de esqueleto para sus subclases. Puede declarar miembros concretos y miembros abstract que las subclases están obligadas a implementar:

abstract class Mutante {
  constructor(
    public name: string,
    public realName: string
  ) {}
}

class Xmen extends Mutante {
  public saveWorld() {
    return 'Mundo salvado'
  }
}

class Villano extends Mutante {
  public conquistWorld() {
    return 'Mundo conquistado'
  }
}

const wolverine = new Xmen('Wolverine', 'Logan')
const magneto = new Villano('Magneto', 'Magnus')

Una ventaja adicional: una función puede aceptar cualquier objeto que herede de la clase abstracta, lo que habilita el polimorfismo:

const printName = (character: Mutante) => {
  console.log(character.name)
}

printName(wolverine)
printName(magneto)

super: invocar a la clase padre

Cuando una subclase sobrescribe un método del padre, puede invocar la versión original con super.metodo(). Si la subclase define un constructor, debe llamar a super() para inicializar correctamente la cadena de herencia:

class Xmen extends Avenger {
  constructor(name: string, realName: string, public isMutant: boolean) {
    super(name, realName)
  }

  public getFullNameDesdeXmen() {
    return super.getFullName()
  }
}

Getters y setters

Los accessors exponen métodos que se comportan sintácticamente como atributos, facilitando la lectura y asignación controladas:

class Xmen extends Avenger {
  get fullName() {
    return `${this.name} - ${this.realName}`
  }

  set fullName(name: string) {
    this.realName = name
  }
}

const wolverine = new Xmen('Wolverine', 'Logan', true)
console.log(wolverine.fullName)     // se lee como atributo
wolverine.fullName = 'David'        // se asigna como atributo

Miembros estáticos

Los miembros static pertenecen a la clase, no a sus instancias:

class Avenger {
  static avgAge: number = 35
  static getAvgAge() {
    return Avenger.avgAge
  }
}

console.log(Avenger.avgAge)         // se accede vía la clase

this como tipo de retorno

Cuando un método retorna la propia instancia, su tipo de retorno es this. Esto es la base del encadenamiento de métodos (method chaining):

class Set {
  add(value: number): this {
    // ...
    return this
  }
}

Tipado estructural

TypeScript verifica las clases por estructura, no por nombre. Dos tipos compatibles en forma son intercambiables, con una salvedad: los miembros private rompen la compatibilidad estructural:

class A {
  private x = 1
}
class B extends A {}

function f(a: A) {}

f(new A())   // OK
f(new B())   // OK
f({ x: 1 })  // Error TS2345: 'x' es private en 'A' pero no en '{x: number}'.

Constructor privado

Un constructor private impide instanciar la clase desde fuera, lo que permite controlar su creación. Es la base del patrón singleton:

class Apocalipsis {
  private static instance: Apocalipsis

  private constructor(public name: string) {}

  static getInstance(): Apocalipsis {
    if (!Apocalipsis.instance) {
      Apocalipsis.instance = new Apocalipsis('Soy Apocalipsis... el único')
    }
    return Apocalipsis.instance
  }
}

const apocalipsis = Apocalipsis.getInstance()

Patrones de diseño habilitados

Factory Pattern

Centraliza la creación de objetos detrás de una función que decide la clase concreta:

type Shoe = { purpose: string }

class BalletFlat implements Shoe { purpose = 'dancing' }
class Boot implements Shoe { purpose = 'woodcutting' }
class Sneaker implements Shoe { purpose = 'walking' }

let Shoe = {
  create(type: 'balletFlat' | 'boot' | 'sneaker'): Shoe {
    switch (type) {
      case 'balletFlat': return new BalletFlat()
      case 'boot': return new Boot()
      case 'sneaker': return new Sneaker()
    }
  }
}

Builder Pattern

Construye un objeto paso a paso mediante métodos que retornan this, habilitando una API fluida:

class RequestBuilder {
  private data: object | null = null
  private method: 'get' | 'post' | null = null
  private url: string | null = null

  setMethod(method: 'get' | 'post'): this {
    this.method = method
    return this
  }
  setData(data: object): this {
    this.data = data
    return this
  }
  setURL(url: string): this {
    this.url = url
    return this
  }

  send() {
    // ...
  }
}

new RequestBuilder().setURL('/api').setMethod('post').setData({}).send()

Conclusión

Las clases de TypeScript ofrecen un modelo de orientación a objetos completo: herencia, modificadores de acceso, clases abstractas para polimorfismo, accessors, miembros estáticos y un tipado estructural con la salvedad de los miembros privados. Estas piezas son el fundamento de patrones de diseño como Factory, Builder y Singleton.

En el siguiente artículo abordaremos los genéricos, la herramienta para escribir código reutilizable sin sacrificar la seguridad de tipos.

Etiquetas :