Clases en TypeScript
Lectura previa: Clases (MDN) ↗
TypeScript ofrece soporte completo para la palabra clave class
introducida en ES2015.
Al igual que con otras características del lenguaje JavaScript, TypeScript agrega anotaciones de tipo y otras sintaxis para permitirte expresar relaciones entre clases y otros tipos.
Miembros de clases
Aquí está la clase más básica, una vacía:
class Point {}
Esta clase aún no es muy útil, así que comencemos a agregar algunos miembros.
Campos
Una declaración de campo crea una propiedad pública editable en una clase:
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
Al igual que con otras ubicaciones, la anotación de tipo es opcional, pero será implícitamente any
si no se especifica.
Los campos también pueden tener inicializadores; estos se ejecutarán automáticamente cuando se cree una instancia de la clase:
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// Imprime 0, 0
console.log(`${pt.x}, ${pt.y}`);
Al igual que con const
, let
y var
, el inicializador de una propiedad de clase se usará para inferir su tipo:
const pt = new Point();
pt.x = "0";
Type 'string' is not assignable to type 'number'.
La opción --strictPropertyInitialization
La configuración strictPropertyInitialization
↗ controla si los campos de clase deben inicializarse en el constructor.
class BadGreeter {
name: string;
}
Property 'name' has no initializer and is not definitely assigned in the constructor.
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
Ten en cuenta que el campo debe inicializarse en el propio constructor. TypeScript no analiza los métodos que invocas desde el constructor para detectar inicializaciones, porque una clase derivada podría sobrescribir esos métodos y no inicializar los miembros.
Si tienes la intención de inicializar definitivamente un campo a través de medios distintos al constructor (por ejemplo, tal vez una biblioteca externa esté completando parte de tu clase por ti), puedes usar el operador de aserción de asignación, !
:
class OKGreeter {
// No inicializado, pero sin error
name!: string;
}
El prefijo readonly
Los campos pueden tener el prefijo readonly
.
Esto evita asignaciones al campo fuera del constructor.
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
}
}
const g = new Greeter();
g.name = "also not ok";
Cannot assign to 'name' because it is a read-only property.
Constructores
Lectura previa: Constructor (MDN) ↗
Los constructores de clases son muy similares a las funciones. Puede agregar parámetros con anotaciones de tipo, valores predeterminados y sobrecargas:
class Point {
x: number;
y: number;
// Firma normal con valores por defecto.
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Point {
// Sobrecargas
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
Solo existen algunas diferencias entre las firmas de constructores de clases y las firmas de funciones:
- Los constructores no pueden tener parámetros de tipo; estos pertenecen a la declaración de clase externa, sobre la cual aprenderemos más adelante.
- Los constructores no pueden tener anotaciones de tipo de retorno: el tipo de instancia de clase siempre es lo que se devuelve
Llamadas a super
Al igual que en JavaScript, si tienes una clase base, necesitarás llamar a super();
en el cuerpo de tu constructor antes de usar cualquier miembro de this.
:
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// Imprime un valor incorrecto en ES5; lanza una excepción en ES6
console.log(this.k);
super();
}
}
'super' must be called before accessing 'this' in the constructor of a derived class.
Olvidarse de llamar a super
es un error fácil de cometer en JavaScript, pero TypeScript te dirá cuándo es necesario.
Métodos
Lectura previa: Definiciones de métodos ↗
Una propiedad de función en una clase se llama método. Los métodos pueden usar todas las anotaciones del mismo tipo como funciones y constructores:
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
Aparte de las anotaciones de tipo estándar, TypeScript no agrega nada más nuevo a los métodos.
Ten en cuenta que dentro del cuerpo de un método, todavía es obligatorio acceder a los campos y otros métodos a través de this
.
Un nombre no calificado en el cuerpo de un método siempre se referirá a algo en el ámbito adjunto:
let x: number = 0;
class C {
x: string = "hello";
m() {
// Esto intenta modificar 'x' de la línea 1, no la propiedad de clase
x = "world";
}
}
Type 'string' is not assignable to type 'number'.
Getters / Setters
Las clases también pueden tener accesorios:
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
Ten en cuenta que un par get/set respaldado por campos sin lógica adicional rara vez es útil en JavaScript. Está bien exponer campos públicos si no necesitas agregar lógica adicional durante las operaciones get/set.
TypeScript tiene algunas reglas de inferencia especiales para los descriptores de acceso:
- Si
get
existe pero noset
, la propiedad es automáticamentereadonly
- Si no se especifica el tipo de parámetro de establecimiento (setter), se infiere del tipo de retorno del getter.
- Los getters y setters deben tener la misma visibilidad de miembros
Desde TypeScript 4.3 ↗, es posible tener descriptores de acceso con diferentes tipos para obtener y asignar.
class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// No permite NaN, Infinity, etc
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
Firmas de índices
Las clases pueden declarar firmas de índice; estos funcionan igual que Firmas de índice para otros tipos de objetos:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
Debido a que el tipo de firma de índice también debe capturar los tipos de métodos, no es fácil utilizar estos tipos de manera útil. Generalmente es mejor almacenar los datos indexados en otro lugar en lugar de en la instancia de la clase misma.
Herencia de clase
Al igual que otros lenguajes con características orientadas a objetos, las clases en JavaScript pueden heredar de las clases base.
Cláusulas implements
Puedes usar una cláusula de implements
para verificar que una clase satisface una interface
particular.
Se emitirá un error si una clase no logra implementarla correctamente:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
pong() {
console.log("pong!");
}
}
Class 'Ball' incorrectly implements interface 'Pingable'.
Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
Las clases también pueden implementar múltiples interfaces, p. class C implements A, B {
.
Precauciones
Es importante entender que una cláusula implements
es solo una verificación de que la clase puede ser tratada como el tipo de interfaz.
No cambia el tipo de clase ni sus métodos en absoluto.
Una fuente común de error es asumir que una cláusula implements
cambiará el tipo de clase - ¡no es así!
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
// No hay errores aquí.
return s.toLowerCase() === "ok";
}
}
Parameter 's' implicitly has an 'any' type.
En este ejemplo, quizás esperábamos que el tipo de s
estuviera influenciado por el parámetro name: string
de check
.
No lo es: las cláusulas implements
no cambian la forma en que se verifica el cuerpo de la clase ni se infiere su tipo.
De manera similar, implementar una interfaz con una propiedad opcional no crea esa propiedad:
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.
Cláusulas extends
Lectura previa: la palabra clave extends (MDN) ↗
Las clases pueden “extenderse” desde una clase base. Una clase derivada tiene todas las propiedades y métodos de su clase base y también puede definir miembros adicionales.
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// Método de la clase base.
d.move();
// Método de la clase derivada.
d.woof(3);
Sobrescribiendo métodos
Lectura previa: palabra clave super (MDN) ↗
Una clase derivada también puede sobrescribir un campo o propiedad de una clase base.
Puedes utilizar la sintaxis super.
para acceder a los métodos de la clase base.
Ten en cuenta que debido a que las clases de JavaScript son un objeto de búsqueda simple, no existe la noción de un “super campo”.
TypeScript exige que una clase derivada sea siempre un subtipo de su clase base.
Por ejemplo, aquí tienes una forma legal de sobrescribir un método:
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
Es importante que una clase derivada siga su contrato de clase base. Recuerda que es muy común (¡y siempre legal!) hacer referencia a una instancia de clase derivada a través de una referencia de clase base:
// Alias la instancia derivada a través de una referencia de clase base
const b: Base = d;
// Ningún problema
b.greet();
¿Qué pasaría si Derived
no siguiera el contrato de Base
?
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// Hacer que este parámetro sea obligatorio
greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
Type '(name: string) => void' is not assignable to type '() => void'.
Si compilamos este código a pesar del error, este ejemplo fallaría:
const b: Base = new Derived();
// Se bloquea porque el "nombre" no estará definido
b.greet();
Declaraciones de campos type-only
Cuando target >= ES2022
o useDefineForClassFields
↗ es true
, los campos de clase se inicializan después de que el constructor de la clase madre se completa, sobrescribiendo cualquier valor establecido por la clase madre. Esto puede ser un problema cuando sólo deseas volver a declarar un tipo más preciso para un campo heredado. Para manejar estos casos, puedes escribir declare
para indicar a TypeScript que no debería haber ningún efecto de tiempo de ejecución para esta declaración de campo.
interface Animal {
dateOfBirth: any;
}
interface Dog extends Animal {
breed: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
// No emite código JavaScript,
// solo garantiza que los tipos sean correctos
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
Orden de inicialización
El orden en que se inicializan las clases de JavaScript puede resultar sorprendente en algunos casos. Consideremos este código:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// Prints "base", not "derived"
const d = new Derived();
¿Qué pasó aquí?
El orden de inicialización de clases, tal como lo define JavaScript, es:
- Los campos de la clase base están inicializados.
- Se ejecuta el constructor de la clase base.
- Los campos de clase derivada se inicializan.
- Se ejecuta el constructor de la clase derivada.
Esto significa que el constructor de la clase base vio su propio valor para name
durante su propio constructor, porque las inicializaciones del campo de la clase derivada aún no se habían ejecutado.
Heredar tipos integrados
Nota: Si no planeas heredar de tipos integrados como
Array
,Error
,Map
, etc. o si tu objetivo de compilación está configurado explícitamente enES6
/ES2015
o superior, puedes saltarte esta sección
En ES2015, los constructores que devuelven un objeto sustituyen implícitamente el valor de this
por cualquier llamador de super(...)
.
Es necesario que el código constructor generado capture cualquier valor de retorno potencial de super(...)
y lo reemplace con this
.
Como resultado, es posible que las subclasificaciones Error
, Array
y otras ya no funcionen como se esperaba.
Esto se debe al hecho de que las funciones constructoras para Error
, Array
y similares usan new.target
de ECMAScript 6 para ajustar la cadena del prototipo; sin embargo, no hay forma de garantizar un valor para new.target
al invocar un constructor en ECMAScript 5.
Otros compiladores de nivel inferior generalmente tienen la misma limitación de forma predeterminada.
Para una subclase como la siguiente:
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
es posible que encuentres que:
- Los métodos pueden ser
undefined
en los objetos devueltos al construir estas subclases, por lo que llamar asayHello
resultará en un error. instanceof
se dividirá entre las instancias de la subclase y sus instancias, por lo que(new MsgError()) instanceof MsgError
devolveráfalse
.
Como recomendación, puedes ajustar manualmente el prototipo inmediatamente después de cualquier llamada super(...)
.
class MsgError extends Error {
constructor(m: string) {
super(m);
// Establece el prototipo explícitamente.
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}
Sin embargo, cualquier subclase de MsgError
también tendrá que configurar manualmente el prototipo.
Para runtimes que no admiten Object.setPrototypeOf
↗, es posible que puedas utilizar __proto__
↗.
Desafortunadamente, [estas soluciones no funcionarán en Internet Explorer 10 y versiones anteriores ↗](https://msdn.microsoft.com/en-us/library/s4esdbwz(v=vs.94) .aspx).
Se pueden copiar manualmente métodos del prototipo a la instancia misma (es decir, MsgError.prototype
a this
), pero la cadena del prototipo en sí no se puede arreglar.
Visibilidad de miembros
Puedes usar TypeScript para controlar si ciertos métodos o propiedades son visibles para el código fuera de la clase.
public
La visibilidad predeterminada de los miembros de la clase es public
.
Se puede acceder a un miembro public
desde cualquier lugar:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
Debido a que public
ya es el modificador de visibilidad predeterminado, nunca necesitas escribirlo en un miembro de la clase, pero puedes elegir hacerlo por razones de estilo/legibilidad.
protected
Los miembros protected
solo son visibles para las subclases de la clase en la que están declarados.
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// Está bien acceder al miembro protegido aquí.
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName(); // Error
Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Exposición de miembros protected
Las clases derivadas deben seguir sus contratos de clase base, pero pueden optar por exponer un subtipo de clase base con más capacidades.
Esto incluye hacer que los miembros protected
sean public
:
class Base {
protected m = 10;
}
class Derived extends Base {
// Sin modificadores, por defecto es 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
Ten en cuenta que Derived
ya podía leer y escribir libremente m
, por lo que esto no altera significativamente la “seguridad” de esta situación.
Lo principal a tener en cuenta aquí es que en la clase derivada, debemos tener cuidado de repetir el modificador protected
si esta exposición no es intencional.
Acceso protected
entre jerarquías
Diferentes lenguajes de programación orientada a objetos no están de acuerdo sobre si es legal acceder a un miembro protected
a través de una referencia de clase base:
class Base {
protected x: number = 1;
}
class Derived1 extends Base {
protected x: number = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10;
}
f2(other: Derived1) {
other.x = 10;
}
}
Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
Java, por ejemplo, considera que esto es legal. Por otro lado, C# y C++ optaron por que este código fuera ilegal.
TypeScript está del lado de C# y C++ aquí, porque acceder a x
en Derived2
solo debería ser legal desde las subclases de Derived2
, y Derived1
no es una de ellas.
Además, si acceder a x
a través de una referencia Derived1
es ilegal (¡y ciertamente debería serlo!), entonces acceder a través de una referencia de clase base nunca debería mejorar la situación.
Consulta también ¿Por qué no puedo acceder a un miembro protegido desde una clase derivada? ↗ que explica más del razonamiento de C#.
private
private
es como protected
, pero no permite el acceso al miembro ni siquiera desde subclases:
class Base {
private x = 0;
}
const b = new Base();
// No se puede acceder desde fuera de la clase
console.log(b.x);
Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
showX() {
// No se puede acceder en subclases
console.log(this.x);
}
}
Property 'x' is private and only accessible within class 'Base'.
Debido a que los miembros private
no son visibles para las clases derivadas, una clase derivada no puede aumentar su visibilidad:
class Base {
private x = 0;
}
class Derived extends Base {
x = 1;
}
Class 'Derived' incorrectly extends base class 'Base'.
Property 'x' is private in type 'Base' but not in type 'Derived'.
Acceso private
entre instancias
Diferentes lenguajes de programación orientada a objetos no están de acuerdo sobre si diferentes instancias de la misma clase pueden acceder a los miembros private
de cada uno.
Mientras que lenguajes como Java, C#, C++, Swift y PHP lo permiten, Ruby no.
TypeScript permite el acceso private
entre instancias:
class A {
private x = 10;
public sameAs(other: A) {
// Sin error
return other.x === this.x;
}
}
Advertencias
Al igual que otros aspectos del sistema de tipos de TypeScript, private
y protected
solo se aplican durante la verificación de tipos ↗.
Esto significa que las construcciones de tiempo de ejecución de JavaScript como in
o la búsqueda de propiedad simple aún pueden acceder a un miembro private
o protected
:
class MySafe {
private secretKey = 12345;
}
// En un archivo JavaScript...
const s = new MySafe();
// Imprimirá 12345
console.log(s.secretKey);
private
también permite el acceso usando notación entre corchetes durante la verificación de tipos. Esto hace que los campos declarados private
sean potencialmente más fáciles de acceder para cosas como tests unitarios, con el inconveniente de que estos campos son ligeramente privados y no imponen estrictamente la privacidad.
class MySafe {
private secretKey = 12345;
}
const s = new MySafe();
// No permitido durante la verificación de tipo
console.log(s.secretKey);
// OK
console.log(s["secretKey"]);
Property 'secretKey' is private and only accessible within class 'MySafe'.
A diferencia del private
de TypeScripts, los campos privados de JavaScript ↗ (#
) permanecen privados después de la compilación y no proporcionan las trampillas de escape mencionadas anteriormente, como el acceso a la notación entre corchetes, lo que los hace duros y privados.
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {}
}
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() { }
}
Al compilar en ES2021 o menor, TypeScript usará WeakMaps en lugar de #
.
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
Si necesitas proteger los valores de tu clase de actores maliciosos, debes usar mecanismos que ofrezcan privacidad estricta en tiempo de ejecución, como closures, WeakMaps o campos privados. Ten en cuenta que estas comprobaciones de privacidad adicionales durante el tiempo de ejecución podrían afectar el rendimiento.
Miembros estáticos
Lectura previa: Miembros estáticos (MDN) ↗
Las clases pueden tener miembros static
.
Estos miembros no están asociados con una instancia particular de la clase.
Se puede acceder a ellos a través del propio objeto constructor de clase:
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
Los miembros estáticos también pueden usar los mismos modificadores de visibilidad public
, protected
y private
:
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
Property 'x' is private and only accessible within class 'MyClass'.
Los miembros estáticos también se heredan:
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Nombres estáticos especiales
Generalmente no es seguro ni posible sobrescribir propiedades del prototipo Function
.
Debido a que las clases son en sí mismas funciones que se pueden invocar con new
, ciertos nombres static
no se pueden usar.
Las propiedades de funciones como name
, length
y call
no son válidas para definirse como miembros static
:
class S {
static name = "S!";
}
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
¿Por qué no hay clases estáticas?
TypeScript (y JavaScript) no tienen una construcción llamada static class
de la misma manera que, por ejemplo, C#.
Esas construcciones sólo existen porque esos lenguajes obligan a que todos los datos y funciones estén dentro de una clase; como esa restricción no existe en TypeScript, no es necesaria. Una clase con una sola instancia normalmente se representa simplemente como un objeto normal en JavaScript/TypeScript.
Por ejemplo, no necesitamos una sintaxis de “clase estática” en TypeScript porque un objeto normal (o incluso una función de nivel superior) hará el trabajo igual de bien:
// Clase "static" innecesaria
class MyStaticClass {
static doSomething() {}
}
// Preferido (alternativa 1)
function doSomething() {}
// Preferido (alternativa 2)
const MyHelperObject = {
dosomething() {},
};
Bloques static
en clases
Los bloques estáticos te permiten escribir una secuencia de declaraciones con su propio alcance que pueden acceder a campos privados dentro de la clase que los contiene. Esto significa que podemos escribir código de inicialización con todas las capacidades de escribir declaraciones, sin fugas de variables y con acceso completo a las partes internas de nuestra clase.
class Foo {
static #count = 0;
get count() {
return Foo.#count;
}
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
Clases genéricas
Las clases, al igual que las interfaces, pueden ser genéricas.
Cuando se crea una instancia de una clase genérica con new
, sus parámetros de tipo se infieren de la misma manera que en una llamada de función:
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b = new Box("hello!");
const b: Box<string>
Las clases pueden usar restricciones genéricas y valores predeterminados de la misma manera que las interfaces.
Parámetros de tipo en miembros estáticos
Este código no es legal y puede que no sea obvio por qué:
class Box<Type> {
static defaultValue: Type;
}
Static members cannot reference class type parameters.
¡Recuerda que los tipos siempre se borran por completo!
En tiempo de ejecución, solo hay un espacio de propiedad Box.defaultValue
.
Esto significa que configurar Box<string>.defaultValue
(si fuera posible) también cambiaría Box<number>.defaultValue
- no es bueno.
Los miembros static
de una clase genérica nunca pueden hacer referencia a los parámetros de tipo de la clase.
this
en clases en tiempo de ejecución
Lectura previa:palabra clave this (MDN) ↗
Es importante recordar que TypeScript no cambia el comportamiento de ejecución de JavaScript, y que JavaScript es algo famoso por tener algunos comportamientos de ejecución peculiares.
El manejo de this
por parte de JavaScript es realmente inusual:
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Imprime "obj", no "MyClass"
console.log(obj.getName());
En pocas palabras, de forma predeterminada, el valor de this
dentro de una función depende de cómo se llamó a la función.
En este ejemplo, debido a que la función fue llamada a través de la referencia obj
, su valor de this
era obj
en lugar de la instancia de clase.
¡Esto rara vez es lo que quieres que suceda! TypeScript proporciona algunas formas de mitigar o prevenir este tipo de error.
Funciones de flecha
Lectura previa: Funciones de flecha (MDN) ↗
Si tienes una función que a menudo se llama de una manera que pierde su contexto this
, puede tener sentido usar una propiedad de función de flecha en lugar de una definición de método:
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Imprime "MyClass" en lugar de fallar
console.log(g());
Esto tiene algunas compensaciones:
- Se garantiza que el valor
this
será correcto en tiempo de ejecución, incluso para el código que no se verifica con TypeScript. - Esto utilizará más memoria, porque cada instancia de clase tendrá su propia copia de cada función definida de esta manera.
- No puedes usar
super.getName
en una clase derivada, porque no hay ninguna entrada en la cadena del prototipo para recuperar el método de la clase base.
Parámetros this
En la definición de un método o función, un parámetro inicial llamado this
tiene un significado especial en TypeScript.
Estos parámetros se borran durante la compilación:
// Entrada de TypeScript con el parámetro 'this'
function fn(this: SomeType, x: number) {
/* ... */
}
// Salida JavaScript
function fn(x) {
/* ... */
}
TypeScript verifica que llamar a una función con un parámetro this
se haga con un contexto correcto.
En lugar de usar una función de flecha, podemos agregar un parámetro this
a las definiciones de métodos para hacer cumplir estáticamente que el método se llame correctamente:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// Error, fallaría
const g = c.getName;
console.log(g());
The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
Este método hace las compensaciones opuestas al enfoque de la función de flecha:
- Los callers de JavaScript aún pueden usar el método de clase incorrectamente sin darse cuenta.
- Sólo se asigna una función por definición de clase, en lugar de una por instancia de clase.
- Las definiciones de métodos base aún se pueden llamar a través de
super
.
Tipos this
En las clases, un tipo especial llamado this
se refiere dinámicamente al tipo de la clase actual.
Veamos cómo es útil esto:
class Box {
contents: string = "";
set(value: string) {
(method) Box.set(value: string): this
this.contents = value;
return this;
}
}
Aquí, TypeScript infirió que el tipo de retorno de set
era this
, en lugar de Box
.
Ahora hagamos una subclase de Box
:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
const b = a.set("hello");
const b: ClearableBox
También puedes usar this
en una anotación de tipo de parámetro:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
Esto es diferente a escribir other: Box
: si tienes una clase derivada, su método sameAs
ahora solo aceptará otras instancias de esa misma clase derivada:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
class DerivedBox extends Box {
otherContent: string = "?";
}
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
Protecciones de tipo basadas en this
Puedes usar this is Type
en la posición de retorno para métodos en clases e interfaces.
Cuando se mezcla con un tipo de estrechamiento (por ejemplo, declaraciones “if”), el tipo del objeto de destino se limitará al Type
especificado.
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
interface Networked {
host: string;
}
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
if (fso.isFile()) {
fso.content;
const fso: FileRep
} else if (fso.isDirectory()) {
fso.children;
const fso: Directory
} else if (fso.isNetworked()) {
fso.host;
const fso: Networked & FileSystemObject
}
Un caso de uso común para una protección de tipos basada en esto es permitir la validación diferida (lazy) de un campo en particular. Por ejemplo, este caso elimina un undefined
del valor contenido dentro del cuadro cuando se ha verificado que hasValue
es verdadero:
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box();
box.value = "Gameboy";
box.value;
(property) Box<unknown>.value?: unknown
if (box.hasValue()) {
box.value;
(property) value: unknown
}
Propiedades de parámetros
TypeScript ofrece una sintaxis especial para convertir un parámetro de constructor en una propiedad de clase con el mismo nombre y valor.
Estas se denominan propiedades de parámetros y se crean anteponiendo un argumento de constructor con uno de los modificadores de visibilidad public
, private
, pretected
o readonly
.
El campo resultante obtiene esos modificadores:
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// El cuerpo no es necesario.
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
(property) Params.x: number
console.log(a.z);
Property 'z' is private and only accessible within class 'Params'.
Expresiones de clase
Lectura previa: Expresiones de clase (MDN) ↗
Las expresiones de clase son muy similares a las declaraciones de clase. La única diferencia real es que las expresiones de clase no necesitan un nombre, aunque podemos referirnos a ellas mediante cualquier identificador al que terminaron vinculadas:
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
const m = new someClass("Hello, world");
const m: someClass<string>
Firmas de constructor
Las clases de JavaScript se instancian con el operador new
. Dado el tipo de una clase en sí, el tipo de utilidad InstanceType ↗ modela esta operación.
class Point {
createdAt: number;
x: number;
y: number
constructor(x: number, y: number) {
this.createdAt = Date.now()
this.x = x;
this.y = y;
}
}
type PointInstance = InstanceType<typeof Point>
function moveRight(point: PointInstance) {
point.x += 5;
}
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8
Clases y miembros abstract
Las clases, métodos y campos en TypeScript pueden ser abstractos.
Un método abstracto o campo abstracto es aquel al que no se le ha proporcionado una implementación. Estos miembros deben existir dentro de una clase abstracta, de la que no se puede crear una instancia directamente.
La función de las clases abstractas es servir como clase base para las subclases que implementan todos los miembros abstractos. Cuando una clase no tiene miembros abstractos, se dice que es concreta.
Veamos un ejemplo:
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
const b = new Base();
Cannot create an instance of an abstract class.
No podemos crear una instancia de Base
con new
porque es abstracta.
En lugar de ello, necesitamos crear una clase derivada e implementar los miembros abstractos:
class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName();
Observa que si nos olvidamos de implementar los miembros abstractos de la clase base, obtendremos un error:
class Derived extends Base {
// olvidé hacer algo
}
Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
Firmas de construcciones abstractas
A veces quierrás aceptar alguna función constructora de clase que produce una instancia de una clase que deriva de alguna clase abstracta.
Por ejemplo, es posible que desees escribir este código:
function greet(ctor: typeof Base) {
const instance = new ctor();
instance.printName();
}
Cannot create an instance of an abstract class.
TypeScript te dice correctamente que estás intentando crear una instancia de una clase abstracta.
Después de todo, dada la definición de greet
, es perfectamente legal escribir este código, que terminaría construyendo una clase abstracta:
// Mal!
greet(Base);
En lugar de eso, quierrás escribir una función que acepte algo con una firma de constructor:
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);
Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
Cannot assign an abstract constructor type to a non-abstract constructor type.
Ahora TypeScript te informa correctamente qué funciones constructoras de clase se pueden invocar: Derived
puede porque es concreta, pero Base
no.
Relaciones Entre Clases
En la mayoría de los casos, las clases en TypeScript se comparan estructuralmente, al igual que otros tipos.
Por ejemplo, estas dos clases se pueden usar en lugar de la otra porque son idénticas:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
De manera similar, las relaciones de subtipo entre clases existen incluso si no hay una herencia explícita:
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
Esto suena sencillo, pero hay algunos casos que parecen más extraños que otros.
Las clases vacías no tienen miembros. En un sistema de tipos estructurales, un tipo sin miembros es generalmente un supertipo de cualquier otra cosa. Entonces, si escribes una clase vacía (¡no lo hagas!), se puede usar cualquier cosa en su lugar:
class Empty {}
function fn(x: Empty) {
// No puedo hacer nada con 'x', así que no lo haré.
}
// ¡Todo bien!
fn(window);
fn({});
fn(fn);