-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Fix #55632 issue (No type inferrence of callback arguments inside a tuple union type) #60434
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 all commits
9eecc30
762fd14
a6c7faa
7b9ca78
53e70cb
2946236
27b9fbf
63553c4
b6e9dbb
bb58abf
2779cf3
565916e
fbb84c3
8365c5c
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 |
---|---|---|
|
@@ -32029,6 +32029,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
); | ||
} | ||
|
||
function discriminateContextualTypeByArrayElements(node: ArrayLiteralExpression, contextualType: UnionType) { | ||
const key = `D${getNodeId(node)},${getTypeId(contextualType)}`; | ||
const cachedType = getCachedType(key); | ||
if (cachedType) return cachedType; | ||
|
||
const isIndexPropertyLike = (property: Symbol) => /^[-+]?\d+$/.test(property.escapedName as string); | ||
const hasIndexPropertyOutOfRange = (type: Type, length: number) => | ||
some(getPropertiesOfType(type), p => { | ||
if (p.flags & SymbolFlags.Optional || !isIndexPropertyLike(p)) return false; | ||
const index = +p.escapedName; | ||
return index < 0 || index >= length; | ||
}); | ||
const hasIndexPropertyInRange = (type: Type, length: number) => | ||
some(getPropertiesOfType(type), p => { | ||
if (!isIndexPropertyLike(p)) return false; | ||
const index = +p.escapedName; | ||
return index >= 0 || index < length; | ||
}); | ||
Comment on lines
+32037
to
+32049
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 thiiink those are mostly redundant. An algorithm matching closer |
||
|
||
jakebailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const elementsLength = node.elements.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. This ties back to https://github.com/microsoft/TypeScript/pull/60434/files#r1957160962 - you can't use this as-is. This is the length of AST elements, not the length of actual elements in the array. You might want to take a look into |
||
const filteredType = filterType(contextualType, type => { | ||
if (isTupleType(type)) { | ||
return ( | ||
elementsLength >= type.target.minLength && | ||
(!!(type.target.combinedFlags & ElementFlags.Variable) || elementsLength <= type.target.fixedLength) | ||
); | ||
} | ||
return !hasIndexPropertyOutOfRange(type, elementsLength); | ||
}); | ||
|
||
if (filteredType.flags & TypeFlags.Never) return setCachedType(key, filteredType); | ||
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. q: why is 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. There used to be an error with other tests when I returned an empty filteredType but this is no longer the case. The new code returns filteredType now. |
||
if (!(filteredType.flags & TypeFlags.Union)) { | ||
return setCachedType( | ||
key, | ||
isTupleType(filteredType) || hasIndexPropertyInRange(filteredType, elementsLength) ? filteredType : contextualType, | ||
); | ||
} | ||
|
||
return setCachedType( | ||
key, | ||
discriminateTypeByDiscriminableItems( | ||
filteredType as UnionType, | ||
mapDefined(node.elements, (element, index) => { | ||
jakebailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const name = ("" + index) as __String; | ||
return isPossiblyDiscriminantValue(element) && isDiscriminantProperty(filteredType, name) ? | ||
[() => getContextFreeTypeOfExpression(element), name] as const : | ||
undefined; | ||
}), | ||
isTypeAssignableTo, | ||
), | ||
); | ||
} | ||
|
||
// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily | ||
// be "pushed" onto a node using the contextualType property. | ||
function getApparentTypeOfContextualType(node: Expression | MethodDeclaration, contextFlags: ContextFlags | undefined): Type | undefined { | ||
|
@@ -32046,9 +32099,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
t => getObjectFlags(t) & ObjectFlags.Mapped ? t : getApparentType(t), | ||
/*noReductions*/ true, | ||
); | ||
return apparentType.flags & TypeFlags.Union && isObjectLiteralExpression(node) ? discriminateContextualTypeByObjectMembers(node, apparentType as UnionType) : | ||
apparentType.flags & TypeFlags.Union && isJsxAttributes(node) ? discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType) : | ||
apparentType; | ||
if (apparentType.flags & TypeFlags.Union) { | ||
if (isObjectLiteralExpression(node)) return discriminateContextualTypeByObjectMembers(node, apparentType as UnionType); | ||
if (isJsxAttributes(node)) return discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType); | ||
if (isArrayLiteralExpression(node)) return discriminateContextualTypeByArrayElements(node, apparentType as UnionType); | ||
} | ||
return apparentType; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
tupleTypeInference3.ts(16,7): error TS2322: Type '[1, (a: boolean) => void]' is not assignable to type 'T3'. | ||
Type '[1, (a: boolean) => void]' is not assignable to type 'T1'. | ||
Type at position 1 in source is not compatible with type at position 1 in target. | ||
Type '(a: boolean) => void' is not assignable to type '(a: number) => void'. | ||
Types of parameters 'a' and 'a' are incompatible. | ||
Type 'number' is not assignable to type 'boolean'. | ||
tupleTypeInference3.ts(30,22): error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
tupleTypeInference3.ts(35,22): error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
tupleTypeInference3.ts(45,24): error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
tupleTypeInference3.ts(64,7): error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
tupleTypeInference3.ts(70,8): error TS7006: Parameter 'arg' implicitly has an 'any' type. | ||
tupleTypeInference3.ts(80,9): error TS7006: Parameter 'arg' implicitly has an 'any' type. | ||
|
||
|
||
==== tupleTypeInference3.ts (7 errors) ==== | ||
// Repro from #55632 | ||
|
||
type InferArg = | ||
| [1, (a: number) => void] | ||
| [2, (b: string) => void]; | ||
const arg: InferArg = [1, (a) => { }]; | ||
|
||
// More tests | ||
|
||
type T1 = [1, (a: number) => void]; | ||
type T2 = { 0: 1, 1: (a: boolean) => void, 2: number }; | ||
type T3 = T1 | T2; | ||
const v31: T3 = [1, (a) => { a === 0 }]; | ||
const v32: T3 = [1, (a) => { a === true }, 0]; | ||
const v33: T3 = [1, (a) => { a === true }, 0, 0]; | ||
const v34: T3 = [1, (a: boolean) => { a === true }]; // Error | ||
~~~ | ||
!!! error TS2322: Type '[1, (a: boolean) => void]' is not assignable to type 'T3'. | ||
!!! error TS2322: Type '[1, (a: boolean) => void]' is not assignable to type 'T1'. | ||
!!! error TS2322: Type at position 1 in source is not compatible with type at position 1 in target. | ||
!!! error TS2322: Type '(a: boolean) => void' is not assignable to type '(a: number) => void'. | ||
!!! error TS2322: Types of parameters 'a' and 'a' are incompatible. | ||
!!! error TS2322: Type 'number' is not assignable to type 'boolean'. | ||
|
||
type T4 = T3 | [2, (b: string) => void] | ||
const v41: T4 = [1, (a) => { a === 0 }]; | ||
const v42: T4 = [1, (a) => { a === true }, 0]; | ||
const v43: T4 = [2, (a) => { a === "" }]; | ||
|
||
type T5 = T4 | {} | ||
const v52: T5 = [1, (a) => { a === 0 }]; | ||
const v53: T5 = [1, (a) => { a === true }, 0]; | ||
const v54: T5 = [2, (a) => { a === "" }]; | ||
|
||
type T6 = T1 | { 0: 1, 1: (a: boolean) => void, 2?: number } | ||
const v61: T6 = [1, (a) => { a === true }, 0]; | ||
const v62: T6 = [1, (a) => { }]; // Error | ||
~ | ||
!!! error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
|
||
type T7 = [1, (a: number) => void, ...number[]]; | ||
type T8 = T7 | [1, (a: boolean) => void] | ||
const v81: T8 = [1, (a) => { a === 0 }, 0]; | ||
const v82: T8 = [1, (a) => { }]; // Error | ||
~ | ||
!!! error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
|
||
type T9 = [1, (a: number) => void, ...[number, string]] | [1, (b: string) => void, number?]; | ||
const v91: T9 = [1, (a) => { a === 0 }, 0, ""]; | ||
const v92: T9 = [1, (a) => { a === "" }]; | ||
const v93: T9 = [1, (a) => { a === "" }, 0]; | ||
|
||
type T10 = [1, (a: number) => void, ...[number, string]] | [1, (b: string) => void, number?, string?]; | ||
const v101: T10 = [1, (a) => { a === "" }]; | ||
const v102: T10 = [1, (a) => { a === "" }, 0]; | ||
const v103: T10 = [1, (a) => { }, 0, ""]; // Error | ||
~ | ||
!!! error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
|
||
type T11 = [1, (a: number) => void, ...[number, string]] | [1, (b: string) => void, number?, boolean?]; | ||
const v111: T11 = [1, (a) => { a === "" }]; | ||
const v112: T11 = [1, (a) => { a === "" }, 0]; | ||
const v113: T11 = [1, (a) => { a === 0 }, 0, ""]; | ||
const v114: T11 = [1, (a) => { a === "" }, 0, true]; | ||
|
||
type T12 = | ||
| { 1: (arg: File) => void } | ||
| { 0?: number, 1: (arg: Date) => void } | ||
| { 0: (arg: boolean) => void } | ||
| { 0: boolean, 1: (arg: number) => void } | ||
| [null, (arg: string) => void]; | ||
declare function f(a: T12): void; | ||
f([null, (a) => { a === "" }]); | ||
f([true, (a) => { a === 0 }]); | ||
f([(a) => { a === true }]); | ||
f([0, (a) => { a as Date }]); | ||
f([, (a) => { }]); // Error | ||
~ | ||
!!! error TS7006: Parameter 'a' implicitly has an 'any' type. | ||
|
||
declare function f2(_: { 0: number, 1: (arg: number) => void } | { 0: number, 1: (arg: boolean) => void, 2: any }): void; | ||
f2([1, arg => { arg === 0 }] as const); | ||
|
||
declare function f3(_: { 0: number, 1: (arg: number) => void } | { 0: number, 1: (arg: boolean) => void, 2?: any }): void; | ||
f3([1, arg => { }] as const); // Error | ||
~~~ | ||
!!! error TS7006: Parameter 'arg' implicitly has an 'any' type. | ||
|
||
declare function f4(_: [(arg: number) => void] | [(arg: string) => void, true]): void; | ||
f4([arg => { arg === 0 }] as const); | ||
|
||
declare function f5(arg: { 0: null, 1: (arg: number) => void, foo?: string } | [1, (arg: boolean) => void]): void; | ||
f5([null, (arg) => { arg === 0 }] as const); | ||
f5([1, (arg) => { arg === true }] as const); | ||
|
||
declare function f6(arg: { 1: (arg: number) => void, [k: number]: (...args: never) => void } | [1, (arg: boolean) => void]): void; | ||
f6([1, (arg) => { }] as const); // Error | ||
~~~ | ||
!!! error TS7006: Parameter 'arg' implicitly has an 'any' type. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
//// [tests/cases/compiler/tupleTypeInference3.ts] //// | ||
|
||
//// [tupleTypeInference3.ts] | ||
// Repro from #55632 | ||
|
||
type InferArg = | ||
| [1, (a: number) => void] | ||
| [2, (b: string) => void]; | ||
const arg: InferArg = [1, (a) => { }]; | ||
|
||
// More tests | ||
|
||
type T1 = [1, (a: number) => void]; | ||
type T2 = { 0: 1, 1: (a: boolean) => void, 2: number }; | ||
type T3 = T1 | T2; | ||
const v31: T3 = [1, (a) => { a === 0 }]; | ||
const v32: T3 = [1, (a) => { a === true }, 0]; | ||
const v33: T3 = [1, (a) => { a === true }, 0, 0]; | ||
const v34: T3 = [1, (a: boolean) => { a === true }]; // Error | ||
|
||
type T4 = T3 | [2, (b: string) => void] | ||
const v41: T4 = [1, (a) => { a === 0 }]; | ||
const v42: T4 = [1, (a) => { a === true }, 0]; | ||
const v43: T4 = [2, (a) => { a === "" }]; | ||
|
||
type T5 = T4 | {} | ||
const v52: T5 = [1, (a) => { a === 0 }]; | ||
const v53: T5 = [1, (a) => { a === true }, 0]; | ||
const v54: T5 = [2, (a) => { a === "" }]; | ||
|
||
type T6 = T1 | { 0: 1, 1: (a: boolean) => void, 2?: number } | ||
const v61: T6 = [1, (a) => { a === true }, 0]; | ||
const v62: T6 = [1, (a) => { }]; // Error | ||
|
||
type T7 = [1, (a: number) => void, ...number[]]; | ||
type T8 = T7 | [1, (a: boolean) => void] | ||
const v81: T8 = [1, (a) => { a === 0 }, 0]; | ||
const v82: T8 = [1, (a) => { }]; // Error | ||
|
||
type T9 = [1, (a: number) => void, ...[number, string]] | [1, (b: string) => void, number?]; | ||
const v91: T9 = [1, (a) => { a === 0 }, 0, ""]; | ||
const v92: T9 = [1, (a) => { a === "" }]; | ||
const v93: T9 = [1, (a) => { a === "" }, 0]; | ||
|
||
type T10 = [1, (a: number) => void, ...[number, string]] | [1, (b: string) => void, number?, string?]; | ||
const v101: T10 = [1, (a) => { a === "" }]; | ||
const v102: T10 = [1, (a) => { a === "" }, 0]; | ||
const v103: T10 = [1, (a) => { }, 0, ""]; // Error | ||
|
||
type T11 = [1, (a: number) => void, ...[number, string]] | [1, (b: string) => void, number?, boolean?]; | ||
const v111: T11 = [1, (a) => { a === "" }]; | ||
const v112: T11 = [1, (a) => { a === "" }, 0]; | ||
const v113: T11 = [1, (a) => { a === 0 }, 0, ""]; | ||
const v114: T11 = [1, (a) => { a === "" }, 0, true]; | ||
|
||
type T12 = | ||
| { 1: (arg: File) => void } | ||
| { 0?: number, 1: (arg: Date) => void } | ||
| { 0: (arg: boolean) => void } | ||
| { 0: boolean, 1: (arg: number) => void } | ||
| [null, (arg: string) => void]; | ||
declare function f(a: T12): void; | ||
f([null, (a) => { a === "" }]); | ||
f([true, (a) => { a === 0 }]); | ||
f([(a) => { a === true }]); | ||
f([0, (a) => { a as Date }]); | ||
f([, (a) => { }]); // Error | ||
|
||
declare function f2(_: { 0: number, 1: (arg: number) => void } | { 0: number, 1: (arg: boolean) => void, 2: any }): void; | ||
f2([1, arg => { arg === 0 }] as const); | ||
|
||
declare function f3(_: { 0: number, 1: (arg: number) => void } | { 0: number, 1: (arg: boolean) => void, 2?: any }): void; | ||
f3([1, arg => { }] as const); // Error | ||
|
||
declare function f4(_: [(arg: number) => void] | [(arg: string) => void, true]): void; | ||
f4([arg => { arg === 0 }] as const); | ||
|
||
declare function f5(arg: { 0: null, 1: (arg: number) => void, foo?: string } | [1, (arg: boolean) => void]): void; | ||
f5([null, (arg) => { arg === 0 }] as const); | ||
f5([1, (arg) => { arg === true }] as const); | ||
|
||
declare function f6(arg: { 1: (arg: number) => void, [k: number]: (...args: never) => void } | [1, (arg: boolean) => void]): void; | ||
f6([1, (arg) => { }] as const); // Error | ||
|
||
//// [tupleTypeInference3.js] | ||
"use strict"; | ||
// Repro from #55632 | ||
var arg = [1, function (a) { }]; | ||
var v31 = [1, function (a) { a === 0; }]; | ||
var v32 = [1, function (a) { a === true; }, 0]; | ||
var v33 = [1, function (a) { a === true; }, 0, 0]; | ||
var v34 = [1, function (a) { a === true; }]; // Error | ||
var v41 = [1, function (a) { a === 0; }]; | ||
var v42 = [1, function (a) { a === true; }, 0]; | ||
var v43 = [2, function (a) { a === ""; }]; | ||
var v52 = [1, function (a) { a === 0; }]; | ||
var v53 = [1, function (a) { a === true; }, 0]; | ||
var v54 = [2, function (a) { a === ""; }]; | ||
var v61 = [1, function (a) { a === true; }, 0]; | ||
var v62 = [1, function (a) { }]; // Error | ||
var v81 = [1, function (a) { a === 0; }, 0]; | ||
var v82 = [1, function (a) { }]; // Error | ||
var v91 = [1, function (a) { a === 0; }, 0, ""]; | ||
var v92 = [1, function (a) { a === ""; }]; | ||
var v93 = [1, function (a) { a === ""; }, 0]; | ||
var v101 = [1, function (a) { a === ""; }]; | ||
var v102 = [1, function (a) { a === ""; }, 0]; | ||
var v103 = [1, function (a) { }, 0, ""]; // Error | ||
var v111 = [1, function (a) { a === ""; }]; | ||
var v112 = [1, function (a) { a === ""; }, 0]; | ||
var v113 = [1, function (a) { a === 0; }, 0, ""]; | ||
var v114 = [1, function (a) { a === ""; }, 0, true]; | ||
f([null, function (a) { a === ""; }]); | ||
f([true, function (a) { a === 0; }]); | ||
f([function (a) { a === true; }]); | ||
f([0, function (a) { a; }]); | ||
f([, function (a) { }]); // Error | ||
f2([1, function (arg) { arg === 0; }]); | ||
f3([1, function (arg) { }]); // Error | ||
f4([function (arg) { arg === 0; }]); | ||
f5([null, function (arg) { arg === 0; }]); | ||
f5([1, function (arg) { arg === true; }]); | ||
f6([1, function (arg) { }]); // Error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this won't work in a situation like this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And, if I'm not mistaken this won't work either:
I'd try to avoid ignoring a type if it can't be proven to be incompatible. It should be easier to land on a more correct solution this way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed the code so it works now with:
I only discard types (prior to the
discriminateTypeByDiscriminableItems
call) if:So, now, this is ok:
Two questions:
I use
/^[-+]?\d+$/
to match index like property keys (so{ "-1": boolean }
is discarded because it has a negative index). Is it correct?{ 1: (arg: number) => void } | [1, (arg: boolean) => void]
leads to an error with[1, (arg) => { }]
(Parameter 'arg' implicitly has an 'any' type), becausediscriminateTypeByDiscriminableItems
fails to discard the first type of the union. I should fix that as well, no?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it could be discarded if that property is required. It shouldn't be discarded if it's optional though. But I'm not sure if it's really worth discarding them - if somebody wants to write a union type with such a thing and hope for something to be discarded... they should rethink their choices 😅
No, this is correct. Both of those types can provide the type for
(arg) => {}
. So the implicit any is the correct result here.