Skip to content

Do not covariantly mix in constraints from contravarrying positions #43439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 === (<ConditionalTypeNode>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 === (<ConditionalTypeNode>parent).trueType) {
const constraint = getImpliedConstraint(type, (<ConditionalTypeNode>parent).checkType, (<ConditionalTypeNode>parent).extendsType);
if (constraint) {
constraints = append(constraints, constraint);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//// [callOfConditionalTypeWithConcreteBranches.ts]
type Q<T> = number extends T ? (n: number) => void : never;
function fn<T>(arg: Q<T>) {
// Expected: OK
// Actual: Cannot convert 10 to number & T
arg(10);
}
// Legal invocations are not problematic
fn<string | number>(m => m.toFixed());
fn<number>(m => m.toFixed());

// Ensure the following real-world example that relies on substitution still works
type ExtractParameters<T> = "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<T> = number extends T ? (cb: (n: number) => void) => void : never;
function fn2<T>(arg: Q2<T>) {
function useT(_arg: T): void {}
// Expected: OK
arg(arg => useT(arg));
}
// Legal invocations are not problematic
fn2<string | number>(m => m(42));
fn2<number>(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> = V extends (...args: any[]) => any ? (...args: Parameters<V>) => 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); });
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
=== tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts ===
type Q<T> = 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<T>(arg: Q<T>) {
>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<string | number>(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<number>(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<T> = "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<T> = 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<T>(arg: Q2<T>) {
>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<string | number>(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<number>(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> = V extends (...args: any[]) => any ? (...args: Parameters<V>) => 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, --, --))

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
=== tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts ===
type Q<T> = number extends T ? (n: number) => void : never;
>Q : Q<T>
>n : number

function fn<T>(arg: Q<T>) {
>fn : <T>(arg: Q<T>) => void
>arg : Q<T>

// Expected: OK
// Actual: Cannot convert 10 to number & T
arg(10);
>arg(10) : void
>arg : Q<T>
>10 : 10
}
// Legal invocations are not problematic
fn<string | number>(m => m.toFixed());
>fn<string | number>(m => m.toFixed()) : void
>fn : <T>(arg: Q<T>) => void
>m => m.toFixed() : (m: number) => string
>m : number
>m.toFixed() : string
>m.toFixed : (fractionDigits?: number) => string
>m : number
>toFixed : (fractionDigits?: number) => string

fn<number>(m => m.toFixed());
>fn<number>(m => m.toFixed()) : void
>fn : <T>(arg: Q<T>) => 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<T> = "parameters" extends keyof T
>ExtractParameters : ExtractParameters<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<T> = number extends T ? (cb: (n: number) => void) => void : never;
>Q2 : Q2<T>
>cb : (n: number) => void
>n : number

function fn2<T>(arg: Q2<T>) {
>fn2 : <T>(arg: Q2<T>) => void
>arg : Q2<T>

function useT(_arg: T): void {}
>useT : (_arg: T) => void
>_arg : T

// Expected: OK
arg(arg => useT(arg));
>arg(arg => useT(arg)) : void
>arg : Q2<T>
>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<string | number>(m => m(42));
>fn2<string | number>(m => m(42)) : void
>fn2 : <T>(arg: Q2<T>) => void
>m => m(42) : (m: (n: number) => void) => void
>m : (n: number) => void
>m(42) : void
>m : (n: number) => void
>42 : 42

fn2<number>(m => m(42));
>fn2<number>(m => m(42)) : void
>fn2 : <T>(arg: Q2<T>) => 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> = V extends (...args: any[]) => any ? (...args: Parameters<V>) => void : Function;
>X : X<V>
>args : any[]
>args : Parameters<V>

34 changes: 34 additions & 0 deletions tests/cases/compiler/callOfConditionalTypeWithConcreteBranches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
type Q<T> = number extends T ? (n: number) => void : never;
function fn<T>(arg: Q<T>) {
// Expected: OK
// Actual: Cannot convert 10 to number & T
arg(10);
}
// Legal invocations are not problematic
fn<string | number>(m => m.toFixed());
fn<number>(m => m.toFixed());

// Ensure the following real-world example that relies on substitution still works
type ExtractParameters<T> = "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<T> = number extends T ? (cb: (n: number) => void) => void : never;
function fn2<T>(arg: Q2<T>) {
function useT(_arg: T): void {}
// Expected: OK
arg(arg => useT(arg));
}
// Legal invocations are not problematic
fn2<string | number>(m => m(42));
fn2<number>(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> = V extends (...args: any[]) => any ? (...args: Parameters<V>) => void : Function;