Safron
Safron

Reputation: 860

Typescript Using 'this' type in constructors

Use case: I want to model a generic system for look-up tables (a Table class) of instances of a specific class (a Model class).

A minimal example of what I would like to do:

// 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);
//                                        ^^^^^^^^^^^^

Unfortunately, TypeScript complains about the this type in the constructor. Why isn't this allowed? Is there a better way to do this?


Unsafe alternative

For now I'm using the following unsafe alternative.

// 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> {}

Answer to comment

To answer a comment of Liam: a very simple safe example of the this type.

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);

Upvotes: 5

Views: 895

Answers (2)

Martin Drozd&#237;k
Martin Drozd&#237;k

Reputation: 381

I think I was able to propose a solution to your problem. Unfortunately due to the language restrictions, it may not be very elegant, but it is not a bad one either.

Unfortunately, the keyword "this" can not be used as a type so it can not be used in generics, as other answers stated. In your case, you can rewrite your code, and instead of "this", just use the current type, BUT, this will not be the "guarantee" that objects inside your Table will be of the same type, which is what you described as necessary.

Unfortunately, in JavaScript/TypeScript, you can not guarantee that objects in any generic collection are of the same type "by typings", because TypeScript does not provide tools such as covariance, contravariance, and invariance. You have to ensure it using code and checks. This is a known issue for example in promises, where you can return types that you should not. (At leas this is what I know and found just now, not 100 % sure)

To create an invariant table, where all members are of the same type, we have to check every inputted element. I proposed one possible model, where every table accepts a user-defined function that checks what types can be let it and what types are forbidden:

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");

        //...
    }
}

The person guard works as follows:

const personGuard: TypeGuard<Person> = function (person: Person): boolean
{
    return person instanceof Person;
}

personGuard(new Person(...)); //true
personGuard("string" as any as Person); //false

Now you can create your models and persons as follows:

// 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');

I understand that I might have changed your model structure a little bit but I do not know your overall picture and I am sure that if you like this solution, you can change it to your likings, this is just a prototype.

I hope this is what you need.


This part of my answer tries to explain my theory of why you can not use "this" keyword in a constructor as a parameter type.

First of all, you cant use "this" as a type in a function. You can not do this:

function foo(a: this) {} //What is "this"? -> Error

Before I explain further, we need to take a trip back to plain JavaScript. One of the ways to create an object is to "instantiate a function", like this:

function Animal(name) { this.name = name; }
var dog = new Animal("doggo");

This is almost exactly what TypeScript classes are compiled to. You see, this Animal object is a function, but also a constructor for the Animal object.

So, why you can not use "this" keyword in the TypeScript constructor? If you look above, a constructor is compiled into a function, and constructor parameters are just some parameters of a function, and these can not have the type of "this".

However, the TypeScript compiler might be able to figure out the "this" type even is the constructor parameter. This is certainly a good suggestion for a feature for the TypeScript team.

Upvotes: 1

Zer0
Zer0

Reputation: 1690

The Type<ContentType> annotations is used for types only, means Table<this> is not valid, because this is always refering to an instance and not a class/type/interface etc. Means: it is valid to pass this as an argument in table.instances.set(id, this);, but Table<this> is not, you should change this to Table<Model>.

Upvotes: 0

Related Questions