javascript - Typescript Uso del tipo 'this' en constructores
Caso de uso: Quiero modelar un sistema genérico para tablas de consulta (una clase Table) de instancias de una clase específica (una clase Model).
Un ejemplo mínimo de lo que me gustaría hacer:
// Generic part
abstract class Table<T extends Model> {
instances: Map<number, T> = new Map();
}
abstract class Model {
constructor(
public readonly id: number,
public table: Table<this> // Error
) {
table.instances.set(id, this);
}
}
// Example: a table of Person objects
class Person extends Model {
constructor(
id: number,
table: Table<this>, // Error
public name: string
) {
super(id, table);
}
}
class PersonTable extends Table<Person> {}
const personTable = new PersonTable();
const person = new Person(0, personTable, 'John Doe');
// Note: the idea of using `this` as generic type argument is to guarantee
// that other models cannot be added to a table of persons, e.g. this would fail:
// class SomeModel extends Model { prop = 0; }
// const someModel = new SomeModel(1, person.table);
// ^^^^^^^^^^^^
Desafortunadamente, TypeScript se queja del tipo this en el constructor. ¿Por qué no se permite esto? ¿Hay una mejor manera de hacer esto?
Alternativa insegura
Por ahora estoy usando la siguiente alternativa insegura.
// Generic part
abstract class Table<T extends Model> {
instances: Map<number, T> = new Map();
}
abstract class Model {
public table: Table<this>;
constructor(
public readonly id: number,
table: Table<Model>
) {
table.instances.set(id, this);
this.table = table as Table<this>;
}
}
// Example: a table of Person objects
class Person extends Model {
constructor(
id: number,
table: Table<Person>,
public name: string
) {
super(id, table);
}
}
class PersonTable extends Table<Person> {}
Responder al comentario
Para responder a un comentario de Liam: un ejemplo muy simple y seguro del tipo this.
class A {
someInstances: this[] = [];
}
class B extends A {
someProp = 0;
}
const a = new A();
const b = new B();
a.someInstances.push(b);
// This isn't allowed: b.someInstances.push(a);
Respuesta #1
Creo que pude proponer una solución a tu problema. Desafortunadamente, debido a las restricciones del idioma, puede que no sea muy elegante, pero tampoco es mala.
Desafortunadamente, la palabra clave "this" no se puede usar como tipo, por lo que no se puede utilizar en genéricos, como dijeron otras respuestas. En tu caso, puedes reescribir tu código y, en lugar de "this", simplemente usar el tipo actual, PERO, esto no será la "garantía" de que los objetos dentro de tu Tabla serán del mismo tipo, que es lo que describiste como necesario.
Desafortunadamente, en JavaScript/TypeScript, no se puede garantizar que los objetos en cualquier colección genérica sean del mismo tipo "por tipado", porque TypeScript no proporciona herramientas como covarianza, contravarianza e invariancia. Tienes que asegurarlo usando código y verificaciones. Este es un problema conocido, por ejemplo, en las promesas, donde puedes devolver tipos que no deberías. (Al menos esto es lo que sé y acabo de encontrar ahora, no estoy 100 % seguro)
Para crear una tabla invariante, donde todos los miembros sean del mismo tipo, tenemos que verificar cada elemento ingresado. Propuse un modelo posible, donde cada tabla acepta una función definida por el usuario que verifica qué tipos se le permiten y qué tipos están prohibidos:
interface TypeGuard<T>
{
(inputObject: T): boolean;
}
// Generic part
class SingleTypeTable<T>
{
private typeGuard: TypeGuard<T>;
constructor(typeGuard: TypeGuard<T>)
{
this.typeGuard = typeGuard;
}
Add(item: T)
{
//Check the type
if (!this.typeGuard(item))
throw new Error("I do not like this type");
//...
}
}
El guardia de personas trabaja de la siguiente manera:
const personGuard: TypeGuard<Person> = function (person: Person): boolean
{
return person instanceof Person;
}
personGuard(new Person(...)); //true
personGuard("string" as any as Person); //false
Ahora puedes crear tus modelos y personas de la siguiente manera:
// Some abstract model
abstract class Model<T>
{
constructor(
public readonly id: number,
public table: SingleTypeTable<T> //Table of models
)
{
}
}
// Example: a table of Person objects
class Person extends Model<Person>
{
constructor(
id: number,
table: SingleTypeTable<Person>,
public name: string
)
{
super(id, table);
}
}
//Usage
const personTable = new Table<Person>(personGuard);
const person = new Person(0, personTable , 'John Doe');
Entiendo que podría haber cambiado un poco la estructura de tu modelo, pero no conozco tu panorama general y estoy seguro de que si te gusta esta solución, puedes cambiarla a tu gusto, esto es solo un prototipo.
Espero que esto sea lo que necesitas.
Esta parte de mi respuesta intenta explicar mi teoría de por qué no se puede usar la palabra clave "this" como tipo de parámetro en un constructor.
Primero que nada, no puedes usar "this" como un tipo en una función. No puedes hacer esto:
function foo(a: this) {} //What is "this"? -> Error
Antes de explicar más, necesitamos hacer un viaje de regreso a JavaScript puro. Una de las formas de crear un objeto es "instanciar una función", así:
function Animal(name) { this.name = name; }
var dog = new Animal("doggo");
Esto es casi exactamente a lo que se compilan las clases de TypeScript. Verás, este objeto Animal es una función, pero también un constructor para el objeto Animal.
Entonces, ¿por qué no se puede usar la palabra clave "this" en el constructor de TypeScript? Si miras arriba, un constructor se compila en una función, y los parámetros del constructor son solo algunos parámetros de una función, y estos no pueden tener el tipo de "this".
Sin embargo, el compilador de TypeScript podría ser capaz de averiguar el tipo de "this" incluso si es el parámetro del constructor. Esto ciertamente es una buena sugerencia para una característica para el equipo de TypeScript.
Respuesta #2
La anotación Type<ContentType> se usa solo para tipos, lo que significa que Table<this> no es válido, porque this siempre se refiere a una instancia y no a una clase/tipo/interfaz, etc. Es decir: es válido pasar this como argumento en table.instances.set(id, this);, pero Table<this> no lo es, deberías cambiar esto a Table<Model>.
