Reputation: 6365
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
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" });
Upvotes: 3
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