5

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);
7
  • Model is a type, this is an instance of a type. So it's not valid, you want Table<Person> not Table<this> Commented Aug 6, 2020 at 9:29
  • 2
    @Liam Polymorphic this types Commented Aug 6, 2020 at 9:33
  • In the this used in that link, it's just returned as a type (: this), not a generic type (Type<this>). So I'm guessing that type of typing is not supported in generics. Commented Aug 6, 2020 at 9:49
  • 2
    @Liam It is supported in generics, just not in constructors. See my 'unsafe alternative' for an example where it is used in a legal place. Commented Aug 6, 2020 at 9:51
  • 1
    @Liam For a shrug example using the this type, see my edit. Commented Aug 6, 2020 at 10:01

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

"Unfortunately, the keyword "this" can not be used as a type so it can not be used in generics, as other answers stated" It can be used in generics, and I have used it in generics. See my answer to Liam's comment.
Although you do provide (a sketch of) an alternative solution, which might be useful in some cases. I still hope that these type-checks can be done compile-time instead of run-time. You also didn't answer the question as to why 'this' can't be used as a type in a constructor this way, although it can perfectly be used outside the constructor, so I will first wait for a better answer.
Thank you for the correction. Compile-time solution would be the best, I agree.
Regarding the question "why 'this' can't be used as a type in a constructor": You can not use "this" as a parameter type in regular functions and I think because the constructor is also a regular function (after compilation), you can not use it there as well. A JavaScript class is basically when you use "new" keyword on a function and constructor parameters are parameters of that function (very rough explanation).
I explained my theory further in the answer, hope I am not completely wrong but it seems logical.
0

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>.

5 Comments

"this is always referring to an instance and not a class/type/interface". This is incorrect. What about polymorphic this types?
@RobbyCornelissen well you still need to create an instance of the class to execute the functions, since those are non-static functions. And they still refer to the instance even if the context is given through inheritance
The question is: now that we all agree that this can be used as a type in TypeScript, why can it not be used as a type parameter in the constructor?
So you should be adding an answer then @RobbyCornelissen?
@Liam If I had the answer, I would provide it. Just pointing out that there's more to it than just saying that this cannot be used as a type in TypeScript, because it obviously can.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.