简体   繁体   中英

Typescript compile error when assigning class type to a generic variable

I am trying to set a class type to a generic variable in typescript. It shows compile error when using generic version.

export class A<T extends Al> { }

export class B extends A<Bl> { }

export class Al { }

export class Bl extends Al { }

show<T extends A<Al>>() {
    let t: Type<T>;         // "t = B" shows compile error but seem to work
    // let t: Type<A<Al>>;  // "t = B" shows no compile error and still works
    t = B;
}

I have also created a stackblitz example here so you can see how it works.

I expect that if B extends A<Al> and T extends A<Al> , generic version let t: Type<T>; t = B; let t: Type<T>; t = B; to work. Instead I have to use let t: Type<A<Al>>; t = B; let t: Type<A<Al>>; t = B; and I am wondering if I can somehow use generic way?

Thank you in advance.

The example code here is violating several TypeScript best practices so it's hard for me to see what your actual use case is:

  • All of the types you created are equivalent to the empty type, {} , and are therefore structurally equivalent to each other . This is very rarely desirable in practice. Even if the code here is just meant as a toy example, I doubt you mean for A<T> , B , Al and Bl to all be completely identical, and it is likely going to result in unexpected behavior even if you address the problem stated in the question.

  • The generic class A<T> does not depend structurally on its type parameter . Therefore, the type A<Al> is exactly the same type as A<Bl> which is exactly the same type as A<string> (did you think A<string> was impossible because string does not extend Al ? Well, it does, since Al is empty... see previous bullet point). Again, I doubt you mean for this to be true.

Therefore I am going to alter your example code to the following:

export class A<T extends Al> {
  constructor(public t: T) {}
  a: string = "a";
}

export class B extends A<Bl> {
  constructor(t: Bl) {
    super(t);
  }
  b: string = "b";
}

export class Al {
  al: string = "al";
}

export class Bl extends Al {
  bl: string = "bl";
}

Now the distinctly named types actually differ structurally from each other and the generic types depend on their type parameters. This doesn't solve the problem you're asking about, but it does stop the compiler from behaving in bizarre ways when you use your types.

One more that I'll note but won't change:

  • Your show() function is generic in T but doesn't seem to accept or return any parameters dependent on the type T . This is fine since it's just example code, but there's no way for the compiler to infer T from the usage of show() (see "type argument inference" in the documentation ). We'll just have to specify T manually when we call it. But in real-world code you'd want the call signature of show<T>() to depend on T (or you'd remove T from the signature entirely).

Now let's look at what's happening and examine the actual compiler error you get:

function show<T extends A<Al>>() {
  let tGood: Type<A<Al>> = B; // okay

  let tBad: Type<T> = B; // error!
  // 'B' is assignable to the constraint of type 'T',
  // but 'T' could be instantiated with a different
  // subtype of constraint 'A<Al>'.
}

So, tGood works, as you noted. The type Type<A<Al>> means "a constructor of things which are assignable to A<Al> . The value B is a perfectly valid Type<A<Al>> , because it is a constructor, and it makes B instances, which are explicitly defined to extend A<Al> .

Note that " X extends Y ", " X is assignable to Y ", and " X is narrower than (or the same as) Y " are all saying (approximately) the same thing: if you have a value of type X , you can assign it to a variable of type Y . And extension/assignability/narrowerness is not symmetric. If X extends Y , it is not necessarily true that Y extends X . As an example, consider that string is assignable to string | number string | number (eg, const sn: string | number = "x" is okay), but string | number string | number is not assignable to string (eg, const s: string = Math.random()<0.5 ? "x" : 1 is an error).

So now look at tBad . That's an error, because T is some generic type, and all we know about it is that T extends A<Al> . It is not necessarily equal to A<Al> ... and in fact may be a strictly narrower subtype of A<Al> (that is, T extends A<Al> does not imply that A<Al> extends T ). So we can't assign B to Type<A<Al>> , because the instances of B might not end up being instances of T . (The error message explicitly says this: 'T' could be instantiated with a different subtype of constraint 'A<Al>' )

Let's come up with a particular example here:

export class C extends A<Al> {
  constructor(t: Al) {
    super(t);
  }
  c: string = "c";
}
show<C>(); // no error, but B does not create instances of C

You can see that an instance of C has, among other things, a string property named c . But the B constructor doesn't make instances with a c property. I can call show<C>() , specifying T as C . That is accepted because C extends A<Al> , but uh oh... inside of the implementation, let t: Type<T> = B is essentially saying let t: Type<C> = B , which is just not true. Hence the error.

Now since I can't tell what you're trying to do with the example code, I'm not sure how to suggest fixing it. You can use your tGood , of course... removing generics entirely should solve the problem. If you need to keep using generics, then you will probably need to pass in parameters of those generic types. That is, something like this:

function showMaybe<T extends A<Al>>(ctor: Type<T>) {
  let tFine: Type<T> = ctor;
}

If you need a Type<T> you're going to have to pass one in... you can't just hope that B is one. Again, I don't know if the above works for you, but something like it is probably the way to go.

If you just want to silence the compiler warning and are convinced that what you are doing is, in fact, safe, then you can use a type assertion :

function showUnsafe<T extends A<Al>>() {
  let tUnsafe: Type<T> = B as any as Type<T>; // unsafe assertion
}

This will no longer produce an error, but has all the same problems as before. You can still call showUnsafe<C>() , and depending on what you do with tUnsafe you may end up lying to the compiler that a constructed instance is of type C when it is really of type B . When you use type assertions, you shift the responsibility for type safety from the compiler to you, so you only have yourself to blame if the assertion turns out to be false and have unpleasant runtime consequences.


Okay, hope that made sense and is of some help to you. Good luck!

Link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM