-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Improve signature assignability for argument lists of different lengths #49218
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
base: main
Are you sure you want to change the base?
Changes from 6 commits
1819397
67892c3
bd712ab
88027d5
6c5383d
46f2e57
0de8923
beb241d
388ec89
8753248
e357450
29979e2
fd52884
336288a
92d0267
1c24035
608f7c3
61d2612
919fc0e
b920e9a
a8ddfc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19381,7 +19381,29 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
|
||
for (let i = 0; i < paramCount; i++) { | ||
const sourceType = i === restIndex ? getRestTypeAtPosition(source, i) : tryGetTypeAtPosition(source, i); | ||
const targetType = i === restIndex ? getRestTypeAtPosition(target, i) : tryGetTypeAtPosition(target, i); | ||
let targetType = i === restIndex ? getRestTypeAtPosition(target, i) : tryGetTypeAtPosition(target, i); | ||
if (i === restIndex && targetType && sourceType && isTupleType(sourceType) && !sourceType.target.hasRestElement) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're intentionally not fixing the case where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeeah. It's likely that this could be fixed by wrapping the body of this loop, executing it for each union member, and checking if any of the iterations return ~
Hmm, I think this is actually already covered - it's just that for this case we don't need to do anything special, so we forward the type so it can be checked by the latter logic as-is. Maybe I'm missing something though - do you have any particular test case in mind? The main purpose behind the added code is to "adjust" the target tuple types - their lengths etc, as without that we simply fail on required elements checks etc (but when comparing signatures we can actually often ignore extra positions that are not utilized by the source signature). |
||
targetType = mapType(targetType, t => { | ||
if (!isTupleType(t)) { | ||
return t; | ||
} | ||
|
||
const typeArguments = getTypeArguments(t); | ||
const elementTypes: Type[] = []; | ||
|
||
for (let i = 0; i < getTypeReferenceArity(sourceType); i++) { | ||
elementTypes.push( | ||
Andarist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
i < typeArguments.length | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's an excellent question. At first, I wasn't sure if this is correct - it has been a while since I worked on this. I rechecked that and this is intentional. The implied source tuple type, which represents parameters of the source signature, might only have fixed elements - while the target tuple type might be declared using rest. For example: const f1: (...args: [number, string, ...boolean[]] | [string, number, ...boolean[]]) => void = (a, b, c) => {}; Here, we want to compare those contravariant tuples: type Target = [number, string, boolean] | [string, number, boolean]
type Source = [a: string | number, b: string | number, c: boolean] However, at the moment - I'm not entirely sure if all "transitions" that can happen here are OK. I need to analyze this again and think if it's always OK to, for example, create an optional element from some required elements. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trying to review this again, and I'm trying to think if using the source tuple type's element flags to construct the new target tuple types is ok. I was trying to think through the case where the source element is optional, so the new target element will be too. I think this makes sense and is OK to do when we're comparing function signatures strictly, i.e. when we'll compare parameter types contravariantly. However, if we're comparing method signatures, we'll end up comparing the parameter types also covariantly, and in that case I don't know if it makes sense for the new target type element to be marked as optional for that comparison. A perhaps convoluted example I came up with to try and show this: // Counter-example to making target element optional when source element is optional?
type NowSource<T extends boolean[]> = [...rest: [boolean, ...T, n?: number]];
type NowTarget<T extends boolean[]> = [...rest: [boolean, ...T, number | string]];
class A {
method<T extends boolean[]>(...args: NowTarget<T>): void {}
}
class B extends A {
method<T extends boolean[]>(...args: NowSource<T>): void {} // Errors on main, no error on PR
}
declare let func: <T extends boolean[]>(...args: NowTarget<T>) => void;
declare let gunc: <T extends boolean[]>(...args: NowSource<T>) => void;
func = gunc; // Errors on main and PR, ok |
||
? t.target.elementFlags[i] & ElementFlags.Required | ||
? typeArguments[i] | ||
: getElementTypeOfSliceOfTupleType(t, i)! | ||
: undefinedType | ||
); | ||
} | ||
|
||
return createTupleType(elementTypes, elementTypes.map(() => ElementFlags.Required)); | ||
}); | ||
} | ||
if (sourceType && targetType) { | ||
// In order to ensure that any generic type Foo<T> is at least co-variant with respect to T no matter | ||
// how Foo uses T, we need to relate parameters bi-variantly (given that parameters are input positions, | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -6,14 +6,15 @@ tests/cases/conformance/types/rest/genericRestParameters3.ts(18,1): error TS2345 | |||
Source has 0 element(s) but target requires 2. | ||||
tests/cases/conformance/types/rest/genericRestParameters3.ts(23,1): error TS2322: Type '(x: string, y: string) => void' is not assignable to type '(x: string, ...args: [string] | [number, boolean]) => void'. | ||||
Types of parameters 'y' and 'args' are incompatible. | ||||
Type '[string] | [number, boolean]' is not assignable to type '[y: string]'. | ||||
Type '[number, boolean]' is not assignable to type '[y: string]'. | ||||
Source has 2 element(s) but target allows only 1. | ||||
Type '[string] | [number]' is not assignable to type '[y: string]'. | ||||
Type '[number]' is not assignable to type '[y: string]'. | ||||
Type 'number' is not assignable to type 'string'. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To the best of my understanding, this change is correct and welcome under the implemented changes. |
||||
tests/cases/conformance/types/rest/genericRestParameters3.ts(24,1): error TS2322: Type '(x: string, y: number, z: boolean) => void' is not assignable to type '(x: string, ...args: [string] | [number, boolean]) => void'. | ||||
Types of parameters 'y' and 'args' are incompatible. | ||||
Type '[string] | [number, boolean]' is not assignable to type '[y: number, z: boolean]'. | ||||
Type '[string]' is not assignable to type '[y: number, z: boolean]'. | ||||
Source has 1 element(s) but target requires 2. | ||||
Type '[number, boolean] | [string, undefined]' is not assignable to type '[y: number, z: boolean]'. | ||||
Type '[string, undefined]' is not assignable to type '[y: number, z: boolean]'. | ||||
Type at position 0 in source is not compatible with type at position 0 in target. | ||||
Type 'string' is not assignable to type 'number'. | ||||
tests/cases/conformance/types/rest/genericRestParameters3.ts(35,1): error TS2554: Expected 1 arguments, but got 0. | ||||
tests/cases/conformance/types/rest/genericRestParameters3.ts(36,21): error TS2345: Argument of type 'number' is not assignable to parameter of type '(...args: CoolArray<any>) => void'. | ||||
tests/cases/conformance/types/rest/genericRestParameters3.ts(37,21): error TS2345: Argument of type '<T extends any[]>(cb: (...args: T) => void) => void' is not assignable to parameter of type '(...args: CoolArray<any>) => void'. | ||||
|
@@ -69,16 +70,17 @@ tests/cases/conformance/types/rest/genericRestParameters3.ts(59,5): error TS2345 | |||
~~ | ||||
!!! error TS2322: Type '(x: string, y: string) => void' is not assignable to type '(x: string, ...args: [string] | [number, boolean]) => void'. | ||||
!!! error TS2322: Types of parameters 'y' and 'args' are incompatible. | ||||
!!! error TS2322: Type '[string] | [number, boolean]' is not assignable to type '[y: string]'. | ||||
!!! error TS2322: Type '[number, boolean]' is not assignable to type '[y: string]'. | ||||
!!! error TS2322: Source has 2 element(s) but target allows only 1. | ||||
!!! error TS2322: Type '[string] | [number]' is not assignable to type '[y: string]'. | ||||
!!! error TS2322: Type '[number]' is not assignable to type '[y: string]'. | ||||
!!! error TS2322: Type 'number' is not assignable to type 'string'. | ||||
Comment on lines
+73
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a change of the error for this case: declare let f1: (x: string, ...args: [string] | [number, boolean]) => void;
declare let f2: (x: string, y: string) => void;
f1 = f2 With the current changes in this PR, I create a slice from the target tuple and I cap it to the source's length~. I do that for similar reasons that I've outlined in the other comment. The source signature can freely ignore the "extra" arguments so we don't even need to check them here (we'd have to ignore them through some other mechanism anyway) |
||||
f1 = f3; // Error | ||||
~~ | ||||
!!! error TS2322: Type '(x: string, y: number, z: boolean) => void' is not assignable to type '(x: string, ...args: [string] | [number, boolean]) => void'. | ||||
!!! error TS2322: Types of parameters 'y' and 'args' are incompatible. | ||||
!!! error TS2322: Type '[string] | [number, boolean]' is not assignable to type '[y: number, z: boolean]'. | ||||
!!! error TS2322: Type '[string]' is not assignable to type '[y: number, z: boolean]'. | ||||
!!! error TS2322: Source has 1 element(s) but target requires 2. | ||||
!!! error TS2322: Type '[number, boolean] | [string, undefined]' is not assignable to type '[y: number, z: boolean]'. | ||||
!!! error TS2322: Type '[string, undefined]' is not assignable to type '[y: number, z: boolean]'. | ||||
!!! error TS2322: Type at position 0 in source is not compatible with type at position 0 in target. | ||||
!!! error TS2322: Type 'string' is not assignable to type 'number'. | ||||
Comment on lines
+80
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error change comes from the fact that I fill the slice of the target type with undefineds here (when it's shorter than the source's slice): I think this is a good change. This will allow more patterns to be assignable, especially with target signatures using unions of tuples for their rest parameter. After all, the implementation is free to ignore any of the provided arguments etc. This is also already just fine in TS: declare let f1: (a: string) => void
declare let f2: (a: string, b: number) => void
f2 = f1 And since this is OK I think that this one should be too: declare let f1: (x: string, ...args: [string] | [number, boolean]) => void;
// Type '(a: string, b: string | number, c: boolean | undefined) => void' is not assignable to type '(x: string, ...args: [string] | [number, boolean]) => void'.
// Types of parameters 'b' and 'args' are incompatible.
// Type '[string] | [number, boolean]' is not assignable to type '[b: string | number, c: boolean | undefined]'.
// Type '[string]' is not assignable to type '[b: string | number, c: boolean | undefined]'.
// Source has 1 element(s) but target requires 2.
f1 = (a, b, c) => {} So with this change that caused this particular error here to be reported differently we now allow the example above and I added a test case for it:
|
||||
f1 = f4; | ||||
|
||||
// Repro from #26110 | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
=== tests/cases/compiler/restTupleUnionShorterContextualParams.ts === | ||
// repro #48663 | ||
|
||
// showcase how those transitive assignments are OK | ||
const f1: (x: string | number) => void = x => {}; | ||
>f1 : Symbol(f1, Decl(restTupleUnionShorterContextualParams.ts, 3, 5)) | ||
>x : Symbol(x, Decl(restTupleUnionShorterContextualParams.ts, 3, 11)) | ||
>x : Symbol(x, Decl(restTupleUnionShorterContextualParams.ts, 3, 40)) | ||
|
||
const f2: (x: string | number, y: string | number) => void = f1; | ||
>f2 : Symbol(f2, Decl(restTupleUnionShorterContextualParams.ts, 4, 5)) | ||
>x : Symbol(x, Decl(restTupleUnionShorterContextualParams.ts, 4, 11)) | ||
>y : Symbol(y, Decl(restTupleUnionShorterContextualParams.ts, 4, 30)) | ||
>f1 : Symbol(f1, Decl(restTupleUnionShorterContextualParams.ts, 3, 5)) | ||
|
||
const f3: (...args: [number, string] | [string, number]) => void = f2; | ||
>f3 : Symbol(f3, Decl(restTupleUnionShorterContextualParams.ts, 5, 5)) | ||
>args : Symbol(args, Decl(restTupleUnionShorterContextualParams.ts, 5, 11)) | ||
>f2 : Symbol(f2, Decl(restTupleUnionShorterContextualParams.ts, 4, 5)) | ||
|
||
// by extension those should be OK too | ||
const f4: (...args: [number, string] | [string, number]) => void = (item) => {} | ||
>f4 : Symbol(f4, Decl(restTupleUnionShorterContextualParams.ts, 8, 5)) | ||
>args : Symbol(args, Decl(restTupleUnionShorterContextualParams.ts, 8, 11)) | ||
>item : Symbol(item, Decl(restTupleUnionShorterContextualParams.ts, 8, 68)) | ||
|
||
const f5: (...args: [number, string] | [string, number]) => void = (item: number | string) => {} | ||
>f5 : Symbol(f5, Decl(restTupleUnionShorterContextualParams.ts, 9, 5)) | ||
>args : Symbol(args, Decl(restTupleUnionShorterContextualParams.ts, 9, 11)) | ||
>item : Symbol(item, Decl(restTupleUnionShorterContextualParams.ts, 9, 68)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
=== tests/cases/compiler/restTupleUnionShorterContextualParams.ts === | ||
// repro #48663 | ||
|
||
// showcase how those transitive assignments are OK | ||
const f1: (x: string | number) => void = x => {}; | ||
>f1 : (x: string | number) => void | ||
>x : string | number | ||
>x => {} : (x: string | number) => void | ||
>x : string | number | ||
|
||
const f2: (x: string | number, y: string | number) => void = f1; | ||
>f2 : (x: string | number, y: string | number) => void | ||
>x : string | number | ||
>y : string | number | ||
>f1 : (x: string | number) => void | ||
|
||
const f3: (...args: [number, string] | [string, number]) => void = f2; | ||
>f3 : (...args: [number, string] | [string, number]) => void | ||
>args : [number, string] | [string, number] | ||
>f2 : (x: string | number, y: string | number) => void | ||
|
||
// by extension those should be OK too | ||
const f4: (...args: [number, string] | [string, number]) => void = (item) => {} | ||
>f4 : (...args: [number, string] | [string, number]) => void | ||
>args : [number, string] | [string, number] | ||
>(item) => {} : (item: string | number) => void | ||
>item : string | number | ||
|
||
const f5: (...args: [number, string] | [string, number]) => void = (item: number | string) => {} | ||
>f5 : (...args: [number, string] | [string, number]) => void | ||
>args : [number, string] | [string, number] | ||
>(item: number | string) => {} : (item: number | string) => void | ||
>item : string | number | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
=== tests/cases/compiler/restTupleUnionWithRestContextualParams.ts === | ||
const f1: (...args: [number, string, ...boolean[]] | [string, number, ...boolean[]]) => void = (a, b, c) => {}; | ||
>f1 : Symbol(f1, Decl(restTupleUnionWithRestContextualParams.ts, 0, 5)) | ||
>args : Symbol(args, Decl(restTupleUnionWithRestContextualParams.ts, 0, 11)) | ||
>a : Symbol(a, Decl(restTupleUnionWithRestContextualParams.ts, 0, 96)) | ||
>b : Symbol(b, Decl(restTupleUnionWithRestContextualParams.ts, 0, 98)) | ||
>c : Symbol(c, Decl(restTupleUnionWithRestContextualParams.ts, 0, 101)) | ||
|
||
const f2: (x: string, ...args: [string] | [number, boolean]) => void = (a, b, c) => {}; | ||
>f2 : Symbol(f2, Decl(restTupleUnionWithRestContextualParams.ts, 2, 5)) | ||
>x : Symbol(x, Decl(restTupleUnionWithRestContextualParams.ts, 2, 11)) | ||
>args : Symbol(args, Decl(restTupleUnionWithRestContextualParams.ts, 2, 21)) | ||
>a : Symbol(a, Decl(restTupleUnionWithRestContextualParams.ts, 2, 72)) | ||
>b : Symbol(b, Decl(restTupleUnionWithRestContextualParams.ts, 2, 74)) | ||
>c : Symbol(c, Decl(restTupleUnionWithRestContextualParams.ts, 2, 77)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
=== tests/cases/compiler/restTupleUnionWithRestContextualParams.ts === | ||
const f1: (...args: [number, string, ...boolean[]] | [string, number, ...boolean[]]) => void = (a, b, c) => {}; | ||
>f1 : (...args: [number, string, ...boolean[]] | [string, number, ...boolean[]]) => void | ||
>args : [number, string, ...boolean[]] | [string, number, ...boolean[]] | ||
>(a, b, c) => {} : (a: string | number, b: string | number, c: boolean) => void | ||
>a : string | number | ||
>b : string | number | ||
>c : boolean | ||
|
||
const f2: (x: string, ...args: [string] | [number, boolean]) => void = (a, b, c) => {}; | ||
>f2 : (x: string, ...args: [string] | [number, boolean]) => void | ||
>x : string | ||
>args : [string] | [number, boolean] | ||
>(a, b, c) => {} : (a: string, b: string | number, c: boolean | undefined) => void | ||
>a : string | ||
>b : string | number | ||
>c : boolean | undefined | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// @strict: true | ||
// @noEmit: true | ||
|
||
// repro #48663 | ||
|
||
// showcase how those transitive assignments are OK | ||
const f1: (x: string | number) => void = x => {}; | ||
const f2: (x: string | number, y: string | number) => void = f1; | ||
const f3: (...args: [number, string] | [string, number]) => void = f2; | ||
|
||
// by extension those should be OK too | ||
const f4: (...args: [number, string] | [string, number]) => void = (item) => {} | ||
const f5: (...args: [number, string] | [string, number]) => void = (item: number | string) => {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// @strict: true | ||
// @noEmit: true | ||
|
||
const f1: (...args: [number, string, ...boolean[]] | [string, number, ...boolean[]]) => void = (a, b, c) => {}; | ||
|
||
const f2: (x: string, ...args: [string] | [number, boolean]) => void = (a, b, c) => {}; |
Uh oh!
There was an error while loading. Please reload this page.