skip to Main Content

Consider the following pseudo code:

abstract class X {}

class Y extends X {
  static compare(a: Y, b: Y) {
    return a.id - b.id;
  }
}

class Z extends X {
  static compare(a: Z, b: Z) {
    return a.name.localCompare(b.name);
  }
}

function sort<T extends X>(items: T[]) {
  return items.sort(T.compare); //   <-- illegal
}


Y.sort([new Y(), new Y()]);     //  <-- should use Ys compare 
Z.sort([new Z(), new Z()]);     //  <-- should use Zs compare 

The sort function should utilize the respective compare function of the type used to call it. This doesn’t work because T can’t be used like that. How can I change the generic function at the end so I can use the static compare function of Y?

(And before anyone comments, please do not suggest solutions how I could simplify this whole thing and avoid the issue altogether. Obviously, the above is an MCVE-esque example to illustrate the problem and not the full code I’m dealing with.)

2

Answers


  1. Ideally you’d give your X superclass a static method that only allows it to operate on instances of the class. But that would require using a polymorphic this type, and TypeScript currently doesn’t support such types inside static methods. See the longstanding open feature request at microsoft/TypeScript#5863. Until and unless that’s ever implemented, we’ll need to work around it.

    One of the common workarounds is to make the method generic and give it a this parameter that represents a similar constraint. So the static method will only be callable on a class of the right shape.

    So let’s say that we only want to be allowed to call sort() on a class constructor which has a compare method of the appropriate type. That type might be

    interface Comparator<T> {
      compare: (this: void, a: T, b: T) => number;
    }
    

    meaning that we require the class constructor to be a Comparator<T> for some T. (Note the void this context which says compare must be callable without being bound to an object; that can be relaxed if we want, but hopefully that’s good enough.) Then we’d write sort() on X like this:

    abstract class X {
      static sort<T extends X>(this: Comparator<T>, items: T[]) {
        return items.sort(this.compare);
      }
    }
    

    That compiles cleanly; let’s test it out:

    class Y extends X {
      id: number = 0;
      static compare(a: Y, b: Y) {
        return a.id - b.id;
      }
    }
    
    class Z extends X {
      name: string = "";
      static compare(a: Z, b: Z) {
        return a.name.localeCompare(b.name);
      }
    }
    
    Y.sort([new Y(), new Y()]); // okay
    Z.sort([new Z(), new Z()]); // okay
    Z.sort([new Y(), new Z()]); // error
    // ---> ~~~~~~~ Y is not Z
    X.sort([new Y(), new Z()]); // error
    // <-- compare is missing from X
    

    Looks good. Your desired calls are supported, and undesirable calls are rejected. You’re only allowed to call Z.sort() on an array of Z instances, and you’re not allowed to call X.sort() directly at all because X is not a Comparator at all.

    Playground link to code

    Login or Signup to reply.
  2. var ID = 0;
    
    abstract class X {
      static sort<X>(items: X[]) : X[]{
        return items.sort(this.compare);
      }
    
      static compare<X>(a : any, y : any) : number {
        return 0;  // Implement in override class
      }
    }
    
    class Y extends X {
      id : number = Math.floor(Math.random() * 100);
      static compare(a: Y, b: Y) {
        return a.id - b.id;
      }
    }
    
    class Z extends X {
      name = '' + Math.floor(Math.random() * 100);
      static compare(a: Z, b: Z) {
        return a.name.localeCompare(b.name);
      }
    }
    
    var y = Y.sort([new Y(), new Y(),new Y(), new Y(),new Y(), new Y()]);     //  <-- should use Ys compare 
    var z = Z.sort([new Z(), new Z(), new Z(), new Z(), new Z(), new Z()]);     //  <-- should use Zs compare 
    
    console.log(y);
    console.log(z);
    

    Playground link

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search