Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it wrong to use InstanceType on generics

Why is it wrong to use InstanceType on generics? Whether it's covariant or contravariant?

interface Ctor {
  new(): Instance;
}

interface Instance {
  print(): void;
}

function f1<T extends Ctor>(ctor: T) {
  // Error: Type 'Instance' is not assignable to Type 'InstanceType<T>'
  const ins: InstanceType<T> = new ctor();
  ins.print();
}

function f2(ctor: Ctor) {
  // No error
  const ins: InstanceType<Ctor> = new ctor();
  ins.print();
}

Playground Link

like image 212
weaponhe Avatar asked Feb 28 '21 03:02

weaponhe


1 Answers

Here's what I think is the problem: Typescript determines the compile-time type of the expression new ctor() by looking at the type of ctor (which is T), checking that T must have a constructor signature (it does), and then finding what the constructor signature of T returns. But T could be any subtype of Ctor, so its constructor signature could return any subtype of Instance.

There are two reasonable things a compiler could do in this case:

  • Either invent a new type variable (let's call it R extends Instance) and let the type of the expression new ctor() be R,
  • or just let the type of the expression be Instance.

Typescript does the latter, perhaps because Typescript only creates type variables if you tell it to; it never does so implicitly. This gives the expression a weaker type than necessary for the assignment to InstanceType<T> to typecheck, because InstanceType<T> does explicitly create a type variable R (using an infer clause in a conditional type), which may be an arbitrary subtype of Instance.

So the type of the expression new ctor() is Instance, but the assignment target's type annotation is (not really, but in some sense) a type variable R which could be any subtype of Instance, hence there is a type error.

A possible solution is to explicitly create a type variable for the return type, like so:

function f1<R extends Instance, T extends {new(): R}>(ctor: T) {
  const ins: R = new ctor();
  ins.print();
}

That said, this is only worth doing if your function deals with R in some other capacity, such as having a return type involving R. Otherwise, your code using ins has to work for any realisation of R including Instance itself, so you might as well declare its type as Instance anyway. (Likewise, if your function doesn't use T anywhere else, you might as well make the parameter type Ctor instead).

like image 78
kaya3 Avatar answered Nov 12 '22 21:11

kaya3