Home > Mobile >  Workaround to simplify the syntax when calling function classes with generic arguments
Workaround to simplify the syntax when calling function classes with generic arguments

Time:08-21

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.

Playground link to code

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 })
  • Related