31

The following error

Static members cannot reference class type parameters.

results from the following piece of code

abstract class Resource<T> {
    /* static methods */
    public static list: T[] = [];
    public async static fetch(): Promise<T[]> {
        this.list = await service.get();
        return this.list;
    }
    /*  instance methods */ 
    public save(): Promise<T> {
        return service.post(this);
    }
}

class Model extends Resource<Model> {
}

/* this is what I would like, but the because is not allowed because :
"Static members cannot reference class type parameters."
*/

const modelList = await Model.fetch() // inferred type would be Model[]
const availableInstances = Model.list // inferred type would be Model[]
const savedInstance = modelInstance.save() // inferred type would be Model

I think it is clear from this example what I'm trying to achieve. I want be able to call instance and static methods on my inheriting class and have the inheriting class itself as inferred type. I found the following workaround to get what I want:

interface Instantiable<T> {
    new (...args: any[]): T;
}
interface ResourceType<T> extends Instantiable<T> {
    list<U extends Resource>(this: ResourceType<U>): U[];
    fetch<U extends Resource>(this: ResourceType<U>): Promise<U[]>;
}

const instanceLists: any = {} // some object that stores list with constructor.name as key

abstract class Resource {
    /* static methods */
    public static list<T extends Resource>(this: ResourceType<T>): T[] {
        const constructorName = this.name;
        return instanceLists[constructorName] // abusing any here, but it works :(
    }
    public async static fetch<T extends Resource>(this: ResourceType<T>): Promise<T[]> {
        const result = await service.get()
        store(result, instanceLists) // some fn that puts it in instanceLists
        return result;
    }
    /*  instance methods */ 
    public save(): Promise<this> {
        return service.post(this);
    }
}
class Model extends Resource {
}
/* now inferred types are correct */
const modelList = await Model.fetch() 
const availableInstances = Model.list 
const savedInstance = modelInstance.save()

The problem that I have with this is that overriding static methods becomes really tedious. Doing the following:

class Model extends Resource {

    public async static fetch(): Promise<Model[]> {
        return super.fetch();
    } 
}

will result in an error because Model is no longer extending Resource correctly, because of the different signature. I can't think of a way to declare a fetch method without giving me errors, let alone having an intuitive easy way to overload.

The only work around I could get to work is the following:

class Model extends Resource {
    public async static get(): Promise<Model[]> {
        return super.fetch({ url: 'custom-url?query=params' }) as Promise<Model[]>;
    }
}

In my opinion, this is not very nice.

Is there a way to override the fetch method without having to manually cast to Model and do tricks with generics?

1
  • 1
    Just a note that static members can also be Generic but you need to type the static function itself like public async static fetch<T>(), also seen in this related answer: stackoverflow.com/a/65804786/423171 (it may not be relevant for this specific question but it was for me when I google this error) Commented Apr 27, 2022 at 12:53

1 Answer 1

42

You could do something like this:

function Resource<T>() {
  abstract class Resource {
    /* static methods */
    public static list: T[] = [];
    public static async fetch(): Promise<T[]> {
      return null!;
    }
    /*  instance methods */
    public save(): Promise<T> {
      return null!
    }
  }
  return Resource;
}

In the above Resource is a generic function that returns a locally declared class. The returned class is not generic, so its static properties and methods have concrete types for T. You can extend it like this:

class Model extends Resource<Model>() {
  // overloading should also work
  public static async fetch(): Promise<Model[]> {
    return super.fetch();
  }
}

Everything has the types you expect:

 Model.list; // Model[]
 Model.fetch(); // Promise<Model[]>
 new Model().save(); // Promise<Model>

So that might work for you.

The only caveats I can see right now:

  • There's a bit of duplication in class X extends Resource<X>() which is less than perfect, but I don't think you can get contextual typing to allow the second X to be inferred.

  • Locally-declared types tend not to be exportable or used as declarations, so you might need to be careful there or come up with workarounds (e.g., export some structurally-identical or structurally-close-enough type and declare that Resource is that type?).

Anyway hope that helps. Good luck!

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

10 Comments

This is interesting indeed! But yes, I feel I might run into a problem with the locally-declared type export issue. I will need to investigate that part further before getting back at this / accepting your answer :-) On a side note, to me it feels like the Typescript guys should just allow for accessing type parameters in static methods.
Having the static side access the type parameters from the instance side unfortunately wouldn't make sense. In class A<T>, the constructor A needs to be able to create an A<T> for any T. That means that from the static side's point of view, the T is not a concrete type. You can think of the static side of A like interface AStatic {new<T>(): AInstance<T>}. If you add other methods to AStatic, it simply doesn't make sense for them to have access to the T from new. The scope of T doesn't make it there.
I think in order for it to work so that static and instance side had access to the same type parameters, there would need to be one static side for each instance side. In C++ (and maybe C#?) that works because template metaprogramming actually generates new code for each instantiation of the type parameter. In Java and TypeScript you can't do this because there's only one actual static side in the type system for each class. I can imagine changing that, but it don't know if such a suggestion would be well received.
@jcalz i get Type '<T>() => typeof Resource' is not a constructor function type.ts(2507), how to get rid of it?
@jcalz I get a error TS4060: Return type of exported function has or is using private name 'Paths'. I figured it's because, if you don't explicitly state function's return type, it will infer it from what it returns. This function returns a class defined inside the function's scope, hence the error. Any advice? Thanks!
|

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.