0

I can't get the code following to type-check:

type MyFunctionConstructor<T, F extends MyFunction<T>> = new (
  f: (n: number) => T
) => F;

class MyFunction<T> {
  constructor(f: (n: number) => T) {
    this.f = f;
  }

  f: (n: number) => T;

  composeT(g: (t: T) => T) {
    return new (this.constructor as MyFunctionConstructor<T, this>)(n =>
      g(this.f(n))
    );
  }

  composeU<U>(g: (t: T) => U) {
    return new (this.constructor as MyFunctionConstructor<U, this>)(n =>
      g(this.f(n)) // tsc error here, see below
    );
  }
}

class MyFancyFunction<T> extends MyFunction<T> {}

I get the following error:

Type 'this' does not satisfy the constraint 'MyFunction<U>'.
Type 'MyFunction<T>' is not assignable to type 'MyFunction<U>'.
Type 'T' is not assignable to type 'U'.

I don't want to call the constructor by name (i.e., new MyFunction(...)) so that if f is an instance of a subclass of MyFunction (e.g., of FancyFunction) then so are f.composeT(g) and f.composeU(g). The as casting used for the constructor call in composeT isn't working for the more general composeU method that has a generic parameter. How can I deal with the extra generic, U?

(The method for making composeT type-check is comes from this answer. This question is essentially a follow-up that I couldn't fit in a comment.)

fmg
  • 813
  • 8
  • 18
  • 1
    Eh, you can force the compiler into submission, but what do you expect the return type of `composeU` to be? Some kind of [generic polymorphic `this` type](https://github.com/Microsoft/TypeScript/issues/5845)? Those aren't part of the language (as of TS3.1 anyway). – jcalz Oct 19 '18 at 22:33
  • You make a very good point, @jcalz, I have trouble even formulating, precisely, what the return type should be. The link you provided looks relevant, but the details seem to get into type theory well beyond my naive understanding. If nothing else, maybe my question describes a simple example of how the abstractions discussed in that post might be useful, independent of `RxJs`, `Observable`, etc? – fmg Oct 20 '18 at 15:34
  • If all you cared about was the current class, then you want to return a `MyFunction`. But there's no way in TypeScript to say that every possible subclass of `MyFunction` must itself be generic, (imagine `class MyStringFunction extends MyFunction` {}`) so even if you could express something like `this`, it wouldn't be straightforward. Basically this sort of thing needs what's called [higher-kinded types](https://github.com/Microsoft/TypeScript/issues/1213) to express, and TypeScript doesn't have it. So... not sure how to answer this in a satisfying way for you, sorry. – jcalz Oct 20 '18 at 15:57
  • "This is currently impossible because it requires language features --- higher kinded types ---beyond those currently available in Typescript..." is a perfectly valid, concrete, satisfying answer, that I would gladly accept if you feel like posting it as such, together with a brief explanation. Whether I'm satisfied with Typescript's feature set (I am) probably isn't that interesting to the SO community. :) – fmg Oct 20 '18 at 16:39

1 Answers1

2

As I mentioned in the comments, this can't really be expressed in TypeScript's type system (as of TS3.1 anyway). TypeScript is very limited in its ability to represent so-called higher-kinded types.

First, you'd like to say that all subclasses of MyFunction<T> must themselves be generic in T. That is, you wouldn't want to extend the type MyFunction<T>, but the type constructor T ⇒ MyFunction<T>, which converts a type T into a MyFunction<T>. But you can't do that, because there's no general way to refer to type constructors in TypeScript (Microsoft/TypeScript#1213).

Next, assuming you could extend T ⇒ MyFunction<T> and not MyFunction<T>, you'd need TypeScript's polymorphic this to respect that, so that this is also a type constructor and this<X> is a concrete type. And you can't do that either (Microsoft/TypeScript#5845).

Since Microsoft/TypeScript#1213 is still an open issue and is in the "help wanted" state, there's some hope that you will eventually be able to do this. But I wouldn't hold my breath. If you look in that issue you'll see some workarounds that some people use, but in my opinion they are too cumbersome for me to recommend.

You could try something like:

  composeU<U>(g: (t: T) => U): MyFunction<U> {
    return new (this.constructor as any)((n: number) =>
      g(this.f(n)); 
    );
  }

but you will need to explicitly narrow definitions for each subclass if you want to capture the spirit of polymorphic this:

class MyFancyFunction<T> extends MyFunction<T> { }
interface MyFancyFunction<T> {
  composeU<U>(g: (t: T) => U): MyFancyFunction<U>;
}

In the above, we use declaration merging to narrow the composeU method of MyFancyFunction.

Anyway, hope that's of some help. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360