I am aware that typescript doesn't allow a static member of a class to access a generic passed to the class like this
abstract class Base<T> {
static genericMethod(props: [K in keyof T]?: T[K]): [K in keyof T]?: T[K] { // <-- Static members cannot reference class type parameters.
return props
}
}
I need this because a second class, let's say named Child
extends class Base
and inherits its methods with itself passed as a generic
class Child extends Base<Child> {
a?: string
b?: number
c?: number
}
In such a way that I am able to call the inherited methods with the right generic type
Child.genericMethod({ a: 3 }) // <-- I want this to be an error, because type of "a" is "string"
What I have tried
I have tried to wrap Base
into a function, so that its static members can accept the function's generic
function Base<T>() {
return class {
static genericMethod(props: { [K in keyof T]?: T[K] }): { [K in keyof T]?: T[K] } {
return props
}
}
}
class Child extends Base<Child>() {
a?: string
b?: number
c?: number
}
Child.genericMethod({ a: 0 }) // Ok. Error as needed. "a" is not of type "number"
Now it works as i intended, but the syntax got a little ugly.
Context
This static method gets added to the Child
class, which is defined by the user, via a decorator.
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
static genericMethod(props: any) {
return props // <-- this is an example implementation!
}
}
}
// User defined Class
@classDecorator
class Child {
a?: string
b?: number
c?: number
}
Child.genericMethod() // <-- Error. genericMethod doesn't exist!
Doing this way, Typescript has no idea that Child.genericMethod()
exists because decorators only exist at runtime (I hope this will change in the future). To make the compiler aware about the presence of genericMethod
I create a phantom class which the user defined Child
class will extend, inheriting its methods.
abstract class Base {
static genericMethod(props: any): any { //<-- I need to use 'T' here but I can't!
return undefined
}
}
@classDecorator
class Child extends Base {
a?: string
b?: number
c?: number
}
Child.genericMethod() // <-- OK. Now the compiler knows that generichMethod is present.
I need to pass the type of Child
to Base
, because genericMethod
needs it to check for the correct args and return value types.
Which brings us once more to the beginning.
Problem
This is an external API and it doesn't look simple to my eyes. Having some user doing class A extends B<A>()
looks a bit dirty. I think that having to repeat A extends B<A>
isn't the best plus someone using it might forget to call B<A>()
. I'd like the user to simply do class A extends B
which is much easier to remember and read.
Question
Is there some other way around this that I didn't think about? Is there some way to make Base
infer the class it is being extended by and use that as its generic?
Playground Link with some (not working) ideas.
CodePudding user response:
Conceptually you'd like to use the polymorphic this
type to refer to the "current" class, but there is currently no direct support for this
types in static methods in TypeScript; see microsoft/TypeScript#5863 for the relevant feature request.
Luckily there are workarounds mentioned in that GitHub issue as well, such as one in this comment suggesting making the static method generic and give it a this
parameter. For your example, that looks like:
abstract class Base {
static parentMethod<T extends Base>(
this: new (...args: any) => T,
props: { [K in keyof T]?: T[K] }
) {
return props
}
}
The idea is that when someone calls Something.parentMethod()
, the compiler will see that Something
is of the this
type, and so it will try to infer T
such that Something
is of type new (...args: any) => T
. In other words, it will infer T
to be the instance type of the Something
class. And now you can use T
in the props
parameter as desired.
Let's test it out:
class Child extends Base {
a?: string
b?: number
c?: number
}
Child.parentMethod({ a: "", b: 2 }); // okay
Child.parentMethod({ a: 3 }); // error
// ----------------> ~
// number is not assignable to string
Looks good. When you call Child.parentMethod()
, the compiler infers that T
is Child
, and thus the call looks like Base.parentMethod<Child>()
, and the props
argument is of type Partial<Child>
. So {a: "", b: 2}
is accepted, but {a: 3}
is an error because the a
property is not a string
.
CodePudding user response:
What about instead of a static method use a generic function like this:
function genericMethod<T>(props: { [K in keyof T]?: T[K] }): { [K in keyof T]?: T[K] } {
return props
}
genericMethod<Child>({ a: 0 })