Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround for accessing class type arguments in static method in Typescript

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?

like image 818
Maurits Moeys Avatar asked Sep 26 '18 12:09

Maurits Moeys


People also ask

How do you access static methods outside the class?

If we want to access the static numHumans field of the Human class from outside of the Human class this time, we must call the getNumHumans() method because we made the static field private by using the private access modifier, which means the field can only be accessed from within the class its a member of, or belongs ...

Can we pass arguments in static method?

No. Your assumption is wrong. You are are passing array to a method an modifiying there so the array pointing to the same reference and value has been changed. Where as in case of a which is a primitive does'nt have a reference and a copied value passed to the method and original value remains same.

Can we have static class in TypeScript?

The class or constructor cannot be static in TypeScript.

Can static methods be Redeclared?

Yes there's a workaround: use another name. ;-) PHP is not C++, methods are unique by their names, not by their name/arguments/visibility combination. Even then, you cannot overload an object method to a static method in C++ either.


1 Answers

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!

like image 74
jcalz Avatar answered Oct 19 '22 13:10

jcalz