Robin De Schepper
Robin De Schepper

Reputation: 6365

How to make a generic parent constructor that accepts object with keys of child class?

I'd like to make a base class that defines a constructor that allows all the keys of the child class to be passed, but this is unavailable in constructors. Here's what I'd like to achieve:

class BaseClass {
  constructor(props: {[key in keyof typeof this]?: typeof this[key]}) {
    Object.assign(this, props)
  }
}

class User extends BaseClass {
  name: string;
  age: number;
}

const user = new User({name: "John"});

But this errors with:

test.ts:2:60 - error TS2333: 'this' cannot be referenced in constructor arguments.

2   constructor(props: { [key in keyof typeof this]?: typeof this[key] }) {

Is there a way to achieve this? I'm not satisfied with any solutions where I have to supply any information about the child classes in the parent class' constructor.

Upvotes: 3

Views: 159

Answers (2)

jcalz
jcalz

Reputation: 329308

As you noticed, it is currently not allowed to use the polymorphic this type in constructor parameters. There is an open feature request for such support at microsoft/TypeScript#38038. It's open and marked as "awaiting more feedback", so anyone who is interested in seeing this happen might want to go there, give the issue a 👍, and possibly describe their use case and why it's compelling. The more people do that the more chance there is that it would eventually be implemented, but it's not likely to happen overnight and maybe not ever. So until and unless that happens, you'll need to work around it.

The polymorphic this type acts as an implicitly F-bounded generic type parameter. (What's "F-bounded"? That's just the technical term for saying that it is constrained to some Funcion of itself. If you had a generic type parameter X and a generic type type F<T> = ..., then X extends F<X> would mean that X is "F-bounded".) Therefore, in cases where this is not allowed, you could try to work around it with an explicitly F-bounded generic type parameter, like so:

class BaseClass<T extends BaseClass<T>> {
    constructor(props: { [K in keyof T]?: T[K] }) {
        Object.assign(this, props);
    }
}

or equivalently

class BaseClass<T extends BaseClass<T>> {
    constructor(props: Partial<T>) {
        Object.assign(this, props);
    }
}

using the Partial utility type.

That now compiles with no error. Your subclasses would then need to be declared in terms of themselves:

class User extends BaseClass<User> {
    name?: string;
    age?: number;
}

Note that I made the name and age properties optional because the base class doesn't necessarily set them (it asks for Partial<T>). If it did, you'd probably need to use a definite assignment assertion like name!: string or a property declaration like declare age: number, since the compiler does not understand that Object.assign() in the super constructor body will actually initialize the declared properties. This is mostly a digression, though.

And everything else works as expected:

const user = new User({ name: "John" });

Playground link to code

Upvotes: 3

toridoriv
toridoriv

Reputation: 11

You can achieve what you want using a generic type parameter and the keyof keyword in the constructor of the base class.

class BaseClass<T> {
  constructor(props: { [key in keyof T]?: T[key] }) {
    Object.assign(this, props);
  }
}

interface User {
  name?: string;
  age?: number;
}

class User extends BaseClass<User> implements User {}

const user = new User({ name: "John" });

You can read more about Typescript generics here.

Upvotes: 1

Related Questions