microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.75k stars 12.46k forks source link

Generic return type for abstract method giving an error in child class #29779

Closed yulric closed 5 years ago

yulric commented 5 years ago

TypeScript Version: Typescript@3.3.1

Search Terms: generic abstract class method

Code

abstract class Parent<T> {
  abstract getX<U = T & { ax: number }>(x: U): U
}

class Child extends Parent<{ nx: number }> {
  getX(x: { nx: number, ax: number }): { nx: number, ax: number } {
    return x;
  }
}

Expected behavior: No Errors

Actual behavior: Property 'getX' in type 'Child' is not assignable to the same property in base type 'Parent<{ nx: number; }>'. Type '(x: { nx: number; ax: number; }) => { nx: number; ax: number; }' is not assignable to type '<U = { nx: number; } & { ax: number; }>(x: U) => U'. Types of parameters 'x' and 'x' are incompatible. Type 'U' is not assignable to type '{ nx: number; ax: number; }'

Playground Link: https://www.typescriptlang.org/play/index.html#src=abstract%20class%20Parent%3CT%3E%20%7B%0D%0A%20%20abstract%20getX%3CU%20%3D%20T%20%26%20%7B%20ax%3A%20number%20%7D%3E(x%3A%20U)%3A%20U%0D%0A%7D%0D%0A%0D%0Aclass%20Child%20extends%20Parent%3C%7B%20nx%3A%20number%20%7D%3E%20%7B%0D%0A%20%20getX(x%3A%20%7B%20nx%3A%20number%2C%20ax%3A%20number%20%7D)%3A%20%7B%20nx%3A%20number%2C%20ax%3A%20number%20%7D%20%7B%0D%0A%20%20%20%20return%20x%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A

Related Issues:

RyanCavanaugh commented 5 years ago

This is a correct error. Parent's contract claims that this call is legal

const p: Parent<{ nx: number }> = new Child();
p.getX<{}>({});

Child's signature for getX does not support that call, e.g. this code reads two undefineds:

class Child extends Parent<{ nx: number }> {
  getX(x: { nx: number, ax: number }): { nx: number, ax: number } {
    return { nx: x.nx + x.ax, ax: 0 };
  }
}
yulric commented 5 years ago

Sorry, I'm not sure what you mean by "this code reads two undefined's"?

jack-williams commented 5 years ago

The call p.getX<{}>({}); passes in an empty object, so in the body of the method the two property accesses:

x.nx + x.ax

will both return undefined because x is empty.

Pmyl commented 5 years ago

I feel like you're trying to use U = T & { ax: number } as a shortcut to define the U generic. That construct is actually used to define a default value for the generic U when is not otherwise specified, that means that I could call p.getX<number>(4) (accepted by the Parent's contract). The problem occurs because Child define that I cannot make that call anymore.

This code should show the error a bit better:

// let's change Child slightly keeping the same contract
// assume that this doesn't give any error
class Child extends Parent<{ nx: number }> {
  getX(x: { nx: number, ax: number }): { nx: number, ax: number } {
    return { nx: 12, ax: 10 };
  }
}
const p: Person<{ nx: number }> = new Child();
console.log(p.getX<number>(4));
// this would not give any error, **the expected logged value is a number**
// but since p is actually an instance of Child is going to log me an object!!!

Remember that when you extends/implements you can only make your generics less specific (allows more values), this is why it fails when you use your code where the generic U becomes more specific (allow less values, in your case it allows only the value { nx: number }). I will show two example to show what I just said:

abstract class Parent {
  abstract getX<A extends number>(x: A): A;
}

class Child extends Parent {
  // ERROR! 5 is more specific than number
  // Type 'number' is not assignable to type '5'.
  getX<A extends 5>(x: A): A { 
    return x;
  }
}
abstract class Parent {
  abstract getX<A extends 5>(x: A): A;
}

class Child extends Parent {
  // IT WORKS! number is less specific than 5
  getX<A extends number>(x: A): A {
    return x;
  }
}

In any case I think you meant to do this:

abstract class Parent<T> {
  abstract getX(x: T & { ax: number }): T & { ax: number }
}

class Child extends Parent<{ nx: number }> {
  getX(x: { nx: number, ax: number }): { nx: number, ax: number } {
    return x;
  }
}
yulric commented 5 years ago

This all makes a lot of sense. Thanks a lot, closing the issue.