diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c6a9f7e75a633..3d9829345eb73 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -12849,9 +12849,17 @@ namespace ts { function getConditionalFlowTypeOfType(type: Type, node: Node) { let constraints: Type[] | undefined; + let covariant = true; while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDocComment) { const parent = node.parent; - if (parent.kind === SyntaxKind.ConditionalType && node === (parent).trueType) { + // only consider variance flipped by parameter locations - `keyof` types would usually be considered variance inverting, but + // often get used in indexed accesses where they behave sortof invariantly, but our checking is lax + if (parent.kind === SyntaxKind.Parameter) { + covariant = !covariant; + } + // Always substitute on type parameters, regardless of variance, since even + // in contravarrying positions, they may be reliant on subtuted constraints to be valid + if ((covariant || type.flags & TypeFlags.TypeParameter) && parent.kind === SyntaxKind.ConditionalType && node === (parent).trueType) { const constraint = getImpliedConstraint(type, (parent).checkType, (parent).extendsType); if (constraint) { constraints = append(constraints, constraint); diff --git a/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.js b/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.js new file mode 100644 index 0000000000000..b3e236ff9cd7b --- /dev/null +++ b/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.js @@ -0,0 +1,53 @@ +//// [callOfConditionalTypeWithConcreteBranches.ts] +type Q = number extends T ? (n: number) => void : never; +function fn(arg: Q) { + // Expected: OK + // Actual: Cannot convert 10 to number & T + arg(10); +} +// Legal invocations are not problematic +fn(m => m.toFixed()); +fn(m => m.toFixed()); + +// Ensure the following real-world example that relies on substitution still works +type ExtractParameters = "parameters" extends keyof T + // The above allows "parameters" to index `T` since all later + // instances are actually implicitly `"parameters" & keyof T` + ? { + [K in keyof T["parameters"]]: T["parameters"][K]; + }[keyof T["parameters"]] + : {}; + +// Original example, but with inverted variance +type Q2 = number extends T ? (cb: (n: number) => void) => void : never; +function fn2(arg: Q2) { + function useT(_arg: T): void {} + // Expected: OK + arg(arg => useT(arg)); +} +// Legal invocations are not problematic +fn2(m => m(42)); +fn2(m => m(42)); + +// webidl-conversions example where substituion must occur, despite contravariance of the position +// due to the invariant usage in `Parameters` + +type X = V extends (...args: any[]) => any ? (...args: Parameters) => void : Function; + +//// [callOfConditionalTypeWithConcreteBranches.js] +function fn(arg) { + // Expected: OK + // Actual: Cannot convert 10 to number & T + arg(10); +} +// Legal invocations are not problematic +fn(function (m) { return m.toFixed(); }); +fn(function (m) { return m.toFixed(); }); +function fn2(arg) { + function useT(_arg) { } + // Expected: OK + arg(function (arg) { return useT(arg); }); +} +// Legal invocations are not problematic +fn2(function (m) { return m(42); }); +fn2(function (m) { return m(42); }); diff --git a/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.symbols b/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.symbols new file mode 100644 index 0000000000000..dbed1d31aa944 --- /dev/null +++ b/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.symbols @@ -0,0 +1,105 @@ +=== tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts === +type Q = number extends T ? (n: number) => void : never; +>Q : Symbol(Q, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 0)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 7)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 7)) +>n : Symbol(n, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 32)) + +function fn(arg: Q) { +>fn : Symbol(fn, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 59)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 1, 12)) +>arg : Symbol(arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 1, 15)) +>Q : Symbol(Q, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 0)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 1, 12)) + + // Expected: OK + // Actual: Cannot convert 10 to number & T + arg(10); +>arg : Symbol(arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 1, 15)) +} +// Legal invocations are not problematic +fn(m => m.toFixed()); +>fn : Symbol(fn, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 59)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 7, 20)) +>m.toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 7, 20)) +>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) + +fn(m => m.toFixed()); +>fn : Symbol(fn, Decl(callOfConditionalTypeWithConcreteBranches.ts, 0, 59)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 8, 11)) +>m.toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 8, 11)) +>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --)) + +// Ensure the following real-world example that relies on substitution still works +type ExtractParameters = "parameters" extends keyof T +>ExtractParameters : Symbol(ExtractParameters, Decl(callOfConditionalTypeWithConcreteBranches.ts, 8, 29)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 11, 23)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 11, 23)) + + // The above allows "parameters" to index `T` since all later + // instances are actually implicitly `"parameters" & keyof T` + ? { + [K in keyof T["parameters"]]: T["parameters"][K]; +>K : Symbol(K, Decl(callOfConditionalTypeWithConcreteBranches.ts, 15, 9)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 11, 23)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 11, 23)) +>K : Symbol(K, Decl(callOfConditionalTypeWithConcreteBranches.ts, 15, 9)) + + }[keyof T["parameters"]] +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 11, 23)) + + : {}; + +// Original example, but with inverted variance +type Q2 = number extends T ? (cb: (n: number) => void) => void : never; +>Q2 : Symbol(Q2, Decl(callOfConditionalTypeWithConcreteBranches.ts, 17, 7)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 8)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 8)) +>cb : Symbol(cb, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 33)) +>n : Symbol(n, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 38)) + +function fn2(arg: Q2) { +>fn2 : Symbol(fn2, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 74)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 13)) +>arg : Symbol(arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 16)) +>Q2 : Symbol(Q2, Decl(callOfConditionalTypeWithConcreteBranches.ts, 17, 7)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 13)) + + function useT(_arg: T): void {} +>useT : Symbol(useT, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 29)) +>_arg : Symbol(_arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 22, 16)) +>T : Symbol(T, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 13)) + + // Expected: OK + arg(arg => useT(arg)); +>arg : Symbol(arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 16)) +>arg : Symbol(arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 24, 6)) +>useT : Symbol(useT, Decl(callOfConditionalTypeWithConcreteBranches.ts, 21, 29)) +>arg : Symbol(arg, Decl(callOfConditionalTypeWithConcreteBranches.ts, 24, 6)) +} +// Legal invocations are not problematic +fn2(m => m(42)); +>fn2 : Symbol(fn2, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 74)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 27, 21)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 27, 21)) + +fn2(m => m(42)); +>fn2 : Symbol(fn2, Decl(callOfConditionalTypeWithConcreteBranches.ts, 20, 74)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 28, 12)) +>m : Symbol(m, Decl(callOfConditionalTypeWithConcreteBranches.ts, 28, 12)) + +// webidl-conversions example where substituion must occur, despite contravariance of the position +// due to the invariant usage in `Parameters` + +type X = V extends (...args: any[]) => any ? (...args: Parameters) => void : Function; +>X : Symbol(X, Decl(callOfConditionalTypeWithConcreteBranches.ts, 28, 24)) +>V : Symbol(V, Decl(callOfConditionalTypeWithConcreteBranches.ts, 33, 7)) +>V : Symbol(V, Decl(callOfConditionalTypeWithConcreteBranches.ts, 33, 7)) +>args : Symbol(args, Decl(callOfConditionalTypeWithConcreteBranches.ts, 33, 23)) +>args : Symbol(args, Decl(callOfConditionalTypeWithConcreteBranches.ts, 33, 49)) +>Parameters : Symbol(Parameters, Decl(lib.es5.d.ts, --, --)) +>V : Symbol(V, Decl(callOfConditionalTypeWithConcreteBranches.ts, 33, 7)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + diff --git a/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.types b/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.types new file mode 100644 index 0000000000000..2bee57b8fe796 --- /dev/null +++ b/tests/baselines/reference/callOfConditionalTypeWithConcreteBranches.types @@ -0,0 +1,99 @@ +=== tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts === +type Q = number extends T ? (n: number) => void : never; +>Q : Q +>n : number + +function fn(arg: Q) { +>fn : (arg: Q) => void +>arg : Q + + // Expected: OK + // Actual: Cannot convert 10 to number & T + arg(10); +>arg(10) : void +>arg : Q +>10 : 10 +} +// Legal invocations are not problematic +fn(m => m.toFixed()); +>fn(m => m.toFixed()) : void +>fn : (arg: Q) => void +>m => m.toFixed() : (m: number) => string +>m : number +>m.toFixed() : string +>m.toFixed : (fractionDigits?: number) => string +>m : number +>toFixed : (fractionDigits?: number) => string + +fn(m => m.toFixed()); +>fn(m => m.toFixed()) : void +>fn : (arg: Q) => void +>m => m.toFixed() : (m: number) => string +>m : number +>m.toFixed() : string +>m.toFixed : (fractionDigits?: number) => string +>m : number +>toFixed : (fractionDigits?: number) => string + +// Ensure the following real-world example that relies on substitution still works +type ExtractParameters = "parameters" extends keyof T +>ExtractParameters : ExtractParameters + + // The above allows "parameters" to index `T` since all later + // instances are actually implicitly `"parameters" & keyof T` + ? { + [K in keyof T["parameters"]]: T["parameters"][K]; + }[keyof T["parameters"]] + : {}; + +// Original example, but with inverted variance +type Q2 = number extends T ? (cb: (n: number) => void) => void : never; +>Q2 : Q2 +>cb : (n: number) => void +>n : number + +function fn2(arg: Q2) { +>fn2 : (arg: Q2) => void +>arg : Q2 + + function useT(_arg: T): void {} +>useT : (_arg: T) => void +>_arg : T + + // Expected: OK + arg(arg => useT(arg)); +>arg(arg => useT(arg)) : void +>arg : Q2 +>arg => useT(arg) : (arg: T & number) => void +>arg : T & number +>useT(arg) : void +>useT : (_arg: T) => void +>arg : T & number +} +// Legal invocations are not problematic +fn2(m => m(42)); +>fn2(m => m(42)) : void +>fn2 : (arg: Q2) => void +>m => m(42) : (m: (n: number) => void) => void +>m : (n: number) => void +>m(42) : void +>m : (n: number) => void +>42 : 42 + +fn2(m => m(42)); +>fn2(m => m(42)) : void +>fn2 : (arg: Q2) => void +>m => m(42) : (m: (n: number) => void) => void +>m : (n: number) => void +>m(42) : void +>m : (n: number) => void +>42 : 42 + +// webidl-conversions example where substituion must occur, despite contravariance of the position +// due to the invariant usage in `Parameters` + +type X = V extends (...args: any[]) => any ? (...args: Parameters) => void : Function; +>X : X +>args : any[] +>args : Parameters + diff --git a/tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts b/tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts new file mode 100644 index 0000000000000..91b236ef61fbd --- /dev/null +++ b/tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts @@ -0,0 +1,34 @@ +type Q = number extends T ? (n: number) => void : never; +function fn(arg: Q) { + // Expected: OK + // Actual: Cannot convert 10 to number & T + arg(10); +} +// Legal invocations are not problematic +fn(m => m.toFixed()); +fn(m => m.toFixed()); + +// Ensure the following real-world example that relies on substitution still works +type ExtractParameters = "parameters" extends keyof T + // The above allows "parameters" to index `T` since all later + // instances are actually implicitly `"parameters" & keyof T` + ? { + [K in keyof T["parameters"]]: T["parameters"][K]; + }[keyof T["parameters"]] + : {}; + +// Original example, but with inverted variance +type Q2 = number extends T ? (cb: (n: number) => void) => void : never; +function fn2(arg: Q2) { + function useT(_arg: T): void {} + // Expected: OK + arg(arg => useT(arg)); +} +// Legal invocations are not problematic +fn2(m => m(42)); +fn2(m => m(42)); + +// webidl-conversions example where substituion must occur, despite contravariance of the position +// due to the invariant usage in `Parameters` + +type X = V extends (...args: any[]) => any ? (...args: Parameters) => void : Function; \ No newline at end of file