microsoft / TypeScript

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

Inconsistent compatibility of generic interfaces #20738

Closed c7hm4r closed 6 years ago

c7hm4r commented 6 years ago

TypeScript Version: 2.7.0-dev.20171216

Code

class Base {}
class Derived extends Base {
  public derived = true;
}

interface ISetter0<T> {
  set(x: T): void;
}
interface ISetter1<T> {
  set: (x: T) => void;
}

const derivedSetter0: ISetter0<Derived> = { set: () => null }; // assignment 0
const derivedSetter1: ISetter1<Derived> = { set: () => null }; // assignment 1

let baseSetter0: ISetter0<Base> = derivedSetter0; // assignment 2
let baseSetter1: ISetter1<Base> = derivedSetter1; // assignment 3

baseSetter0 = derivedSetter1; // assignment 4
baseSetter1 = derivedSetter0; // assignment 5

Compiler options:

  "compilerOptions": {
    "strict": true,
    "strictFunctionTypes": true,
  },

Expected behavior:

Compilation should fail at assignments 2, 3, 4 and 5.

Actual behavior:

Compilation only fails at assigments 3 and 5.

tsc output:

test.ts(17,5): error TS2322: Type 'ISetter1<Derived>' is not assignable to type 'ISetter1<Base>'.
  Type 'Base' is not assignable to type 'Derived'.
    Property 'derived' is missing in type 'Base'.
test.ts(20,1): error TS2322: Type 'ISetter0<Derived>' is not assignable to type 'ISetter1<Base>'.
  Types of property 'set' are incompatible.
    Type '(x: Derived) => void' is not assignable to type '(x: Base) => void'.
      Types of parameters 'x' and 'x' are incompatible.
        Type 'Base' is not assignable to type 'Derived'.

Additional Notes:

The expected behavior handles contravariance correctly, the actual behavior not.

This bug could be part of the more general issue “Covariance / Contravariance Annotations”, but a full covariance / contravariance implementation is probably not necessary to fix this bug. It would probably also be fixed if “Proposal: covariance and contravariance generic type arguments annotations” is implemented.

ahejlsberg commented 6 years ago

This is working as intended. The --strictFunctionTypes mode doesn't affect properties declared using method syntax. See #18654 for the rationale.

c7hm4r commented 6 years ago

To simulate properties with strict type checking currently it seems to be the simplest solution to define a getter and setter, going without a more concise syntax. For example:

interface IBox<T> {
  getValue: () => T;
  setValue: (value: T) => void;
}

Instead of:

interface IBox<T> {
  value: T;
}

To implement the above interface and achieve type safety I think it is necessary to either:

In my opinion it is too restrictive that the contravariance of a function depends on whether it is a method or a property. I would be pleased if strict type checking for methods is introduced in TypeScript in the future—I am used to C#.

RyanCavanaugh commented 6 years ago

Unfortunately the DOM itself is constructed in a way where it can't be correctly typed without either adding write-only properties (ugh) or introducing semantics for inheritance-isn't-subtyping. All the element event methods have a callback that passes the element type itself, which means they can be incorrectly invoked through a supertype reference.

typescript-bot commented 6 years ago

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.