Menu

Clases en TypeScript: campos, métodos, getters e implements

Las clases de TypeScript son clases de JavaScript con campos, métodos y constructores tipados. Aprende cómo funcionan las declaraciones de campos y strictPropertyInitialization, cómo tipar this, getters y setters, miembros estáticos, implements y cómo una clase sirve también de tipo.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una clase de TypeScript es una clase de JavaScript con tipos: cada campo se declara con su tipo en el cuerpo de la clase, y los métodos y el constructor tienen parámetros y valores de retorno tipados.

La salida compilada es una clase normal de JavaScript sin los tipos, así que todo lo de las clases de JavaScript sigue siendo válido. Lo que añade TypeScript ocurre en tiempo de compilación: la última llamada se rechaza antes de que el programa se ejecute.

Declarar campos

Un campo necesita una declaración en el cuerpo de la clase antes de que el constructor pueda asignarlo. Asignar this.owner sin declarar owner es el error TS2339 (la propiedad no existe). Un campo con inicializador recibe el tipo inferido del valor, igual que una variable.

DeclaraciónSignificado
name: string;Debe asignarse en el constructor
count = 0;Inicializado, tipo inferido number
label?: string;Opcional, el tipo es string | undefined
readonly id: number;Se asigna una vez y después solo se lee
data!: string[];Asignación definitiva: confía en mí, se asignará
static total = 0;Pertenece a la clase, no a las instancias

Con strict activado, strictPropertyInitialization comprueba que cada campo no opcional recibe un valor. Esta clase olvida dos:

El compilador informa de los dos:

index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

La comprobación existe porque sin ella new Profile().name.toUpperCase() pasaría la comprobación de tipos y luego fallaría con undefined. Arregla cada campo de una de cuatro formas: dale un inicializador, asígnalo en el constructor, márcalo como opcional con ?, o escribe name!: string cuando un framework o un método init() lo asigne después. La última desactiva la comprobación para ese campo, así que úsala con moderación.

Métodos y this

Los métodos se tipan como funciones. Dentro de un método, this tiene el tipo de la instancia. Un método puede devolver this para permitir el encadenamiento, y el tipo de retorno sigue a las subclases automáticamente.

class QueryBuilder {
    private parts: string[] = [];

    where(condition: string): this {
        this.parts.push(condition);
        return this;
    }

    build(): string {
        return this.parts.join(" AND ");
    }
}

const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1

Un método pierde su this cuando lo separas del objeto, un bug clásico de JavaScript. TypeScript no lo detecta por defecto, pero sí lo hace si el método declara un parámetro this. Ese parámetro se borra de la salida y solo protege el lugar de la llamada:

El campo con función flecha funciona porque cada instancia recibe su propia función, que captura this. El precio es una función por instancia en lugar de un único método compartido en el prototipo.

Getters y setters

Los accesores get y set parecen propiedades desde fuera. Un getter sin setter es de solo lectura: asignarle un valor es un error de compilación (TS2540).

El compilador rechaza la asignación, y si se salta la comprobación el runtime también lanza un TypeError, porque el archivo compilado se ejecuta en modo estricto (el compilador genera "use strict" al principio). El RangeError del setter es una comprobación en tiempo de ejecución que escribiste tú. El tipo number solo dice que el valor es un número; no sabe nada de rangos.

Miembros estáticos

Los campos y métodos static pertenecen a la propia clase. Accedes a ellos a través del nombre de la clase, no de una instancia. Un bloque static { } se ejecuta una vez, cuando se define la clase.

Los miembros estáticos no pueden usar los parámetros de tipo de la clase: en class Box<T>, un static empty: T es el error TS2302, porque solo hay un campo estático, compartido por todos los Box<string>, Box<number> y demás.

Implementar una interfaz

implements le pide al compilador que compruebe que la clase tiene todo lo que exige una interfaz. Un miembro que falta es el error TS2420: Class 'X' incorrectly implements interface 'Y'.

Una clase puede implementar varias interfaces: class Doc implements Printable, Serializable. Hay dos cosas que implements no hace. No tipa por ti los parámetros de los métodos de la clase: check(s) { ... } dentro de una clase que implementa check(s: string): boolean sigue siendo el error TS7006 (Parameter 's' implicitly has an 'any' type), así que anótalos. Y no deja rastro en tiempo de ejecución, así que obj instanceof Shape no es posible.

El private radius del constructor es una propiedad de parámetro: declara y asigna el campo en un solo paso. Consulta constructores.

Una clase también es un tipo

Una declaración de clase crea dos cosas con un mismo nombre: un valor (la función constructora que llamas con new) y un tipo (la forma de una instancia). Como TypeScript compara los tipos por estructura, cualquier objeto con los mismos miembros públicos encaja en el tipo, aunque no lo haya creado la clase.

El objeto normal pasa la comprobación de tipos pero no es un Point en tiempo de ejecución: ({ x: 6, y: 8 }) instanceof Point es false. Una clase con un miembro private o #private lo impide: solo las instancias de esa clase (o de sus subclases) se le pueden asignar.

Errores habituales

  • Declarar un campo y no asignarlo nunca. TS2564 te está diciendo que el campo sería undefined. Inicialízalo en lugar de silenciarlo con !.
  • Pasar un método como callback. button.onclick = obj.handle pierde this. Usa un campo con función flecha u obj.handle.bind(obj).
  • Esperar que implements añada código o tipos. Solo comprueba. Los tipos de los parámetros siguen necesitando anotaciones.
  • Usar instanceof con una interfaz. Las interfaces no existen en tiempo de ejecución; comprueba una clase o usa un type guard.
  • Suponer que los tipos validan los datos. Un campo number acepta cualquier número en tiempo de compilación, y cualquier cosa que llegue de datos sin tipar en tiempo de ejecución.

Preguntas frecuentes

¿Cómo se crea una clase en TypeScript?

Declara cada campo con su tipo en el cuerpo de la clase y después escribe el constructor y los métodos como en JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. Crea una instancia con new User("Ada").

¿Qué significa "has no initializer and is not definitely assigned in the constructor"?

Es el error TS2564 de strictPropertyInitialization (parte de strict). Un campo de tipo string empezaría siendo undefined porque nada lo asigna. Arréglalo con un inicializador (name = ""), una asignación en el constructor, un campo opcional (name?: string) o, cuando algo fuera del constructor lo asigna, una aserción de asignación definitiva (name!: string).

¿Qué diferencia hay entre implements y extends en TypeScript?

extends hereda código de una clase padre: sus campos y métodos existen en la hija en tiempo de ejecución. implements solo le pide al compilador que compruebe que la clase tiene la forma de una interfaz. No añade nada a la clase y desaparece del JavaScript compilado.

¿Se puede usar una clase como tipo en TypeScript?

Sí. El nombre de una clase es a la vez un valor (el constructor) y un tipo (la forma de sus instancias). let u: User acepta cualquier objeto con los mismos miembros públicos, porque TypeScript compara los tipos de forma estructural. typeof User es el tipo del propio constructor.

¿Las clases de TypeScript son distintas de las de JavaScript en tiempo de ejecución?

No. TypeScript compila una clase a una clase normal de JavaScript. Las anotaciones de tipo, las cláusulas implements y modificadores como private se borran; solo existen en tiempo de ejecución las características de JavaScript, como los campos #private, los bloques static y los getters.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR