cc @ahejlsberg
TypeScript Version: master
Code
declare class C {
static a(f: (a: C) => C): void;
static b(): (a: C) => C;
}
declare class D extends C {
private p: void;
static a(f: (a: D) => D): void;
static b(): (a: D) => D;
}
Expected behavior:
no error or both a and b make an error.
Actual behavior:
$ node built/local/tsc.js index.d.ts --noEmit --strictFunctionTypes
index.d.ts(5,15): error TS2417: Class static side 'typeof D' incorrectly extends base class static side 'typeof C'.
Types of property 'b' are incompatible.
Type '() => (a: D) => D' is not assignable to type '() => (a: C) => C'.
Type '(a: D) => D' is not assignable to type '(a: C) => C'.
Types of parameters 'a' and 'a' are incompatible.
Type 'C' is not assignable to type 'D'.
Property 'p' is missing in type 'C'.
I'm not sure this difference is intended or not.
I think you're right, the following should be an error with --strictFunctionTypes:
declare class C {
static a(f: (x: C) => C): void;
}
declare class D extends C {
private p: void;
static a(f: (x: D) => D): void;
}
We currently don't report an error because of overlap in functionality between strict function type checking and covariant checking for callback parameters (#15104). This causes us to always check the return type of a callback parameter bivariantly when we should actually check it contravariantly in strict function types mode.
Definitely a corner case, but one that we should get right.
So, it turns out we can't tighten the rules for the original example. If we did we would break covariance for a number of core types, such as Array<T>, which simply isn't an option. The issue is that many types rely on covariance for the result position of a callback parameter of a method. For example, the reduce method in Array<T>:
interface Array<T> {
// ...
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T;
// ...
}
If we were to strictly (i.e. contravariantly) check the return position of that callback in --strictFunctionTypes mode, Array<T> would become invariant.
However... It is the case that we aren't correctly checking callback parameters of regular function types in strict function types mode. For example, we should error on both assignments below, but we only error on the second one:
declare class C {
private c: void;
}
declare class D extends C {
private d: void;
}
declare let f1: (f: (x: C) => C) => void;
declare let f2: (f: (x: D) => D) => void;
f1 = f2; // Should be an error
f2 = f1; // Error
Most helpful comment
So, it turns out we can't tighten the rules for the original example. If we did we would break covariance for a number of core types, such as
Array<T>, which simply isn't an option. The issue is that many types rely on covariance for the result position of a callback parameter of a method. For example, thereducemethod inArray<T>:If we were to strictly (i.e. contravariantly) check the return position of that callback in
--strictFunctionTypesmode,Array<T>would become invariant.However... It is the case that we aren't correctly checking callback parameters of regular function types in strict function types mode. For example, we should error on both assignments below, but we only error on the second one: