whitestripe
whitestripe

Reputation: 409

Strange behaviour for function call in generic class

The following code compiles fine

interface W {
  x: string;
}
interface M {
  x: string;
}
type D<N extends M> = N | W;

declare function a<N extends M, Q extends D<N> = D<N>>(): Q | Q[] | undefined;
declare function b<N extends M, Q extends D<N> = D<N>>(p?: Q | Q[]): void;

const x = a();
b(x);

Things get interesting when I put function b inside a class:

export class C<N extends M, Q extends D<N>> {
  f(p?: Q | Q[]): void {}

  g() {
    const y = a();
    this.f(a());
    this.f(y);
  }
}

Here, C.f should be equivalent to function b. However, I get a type error on the line this.f(y). Interestingly, the line this.f(a()) compiles fine.

Argument of type 'W | M | (W | M)[] | undefined' is not assignable to parameter of type 'Q | Q[] | undefined'.
  Type 'W' is not assignable to type 'Q | Q[] | undefined'.
    Type 'W' is missing the following properties from type 'Q[]': length, pop, push, concat, and 28 more.

I don't understand this error, because type Q should be equivalent to W | N so how can W not be assignable to Q?

Upvotes: 1

Views: 53

Answers (1)

kaya3
kaya3

Reputation: 51034

The difference is caused by having type parameters on a function, vs. type parameters on a class. To simplify the answer, I'll remove the Q[] option on a, b and f since the same behaviour is exhibited without it.


In the assignment x = a(), Typescript needs to infer a type for the variable x. This requires choosing concrete types for the type variables N and Q; in the absence of other constraints, their upper bounds are chosen, so x gets the type M | W | undefined. The important point here is that the compiler is free to choose the upper bound D<M> as the concrete type for Q.

Then, in the call b(x), Typescript is equally free to choose N = M and Q = D<M>, so there is no type error.


In the class, the assignment y = a() is exactly as above, so y has the type M | W | undefined. However, then in the call to f(y), Typescript is not free to choose N = M and Q = D<M>, because these N and Q are type parameters of the class. It is not sound here to infer N = M and Q = D<M>, because someone:

  • could write class M2 implements M { ... } and create a C<M2, D<M2>> object.
  • could write type D2<N extends M> = D<N> & { ... } and create a C<M, D2<M>> object.

So in the call f(y), it is not safe to assign y of type M | W | undefined to f's parameter of type Q, because e.g. when Q = D<M2>, or Q = D2<M>, or something else, then the variable y is not Q or a subtype of Q.

Upvotes: 1

Related Questions