Skip to content

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
62 changes: 59 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32029,6 +32029,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
);
}

function discriminateContextualTypeByArrayElements(node: ArrayLiteralExpression, contextualType: UnionType) {
const key = `D${getNodeId(node)},${getTypeId(contextualType)}`;
Copy link
Contributor

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:

function test(arg: { 1: (arg: number) => void; foo?: string }) {}
test([null, (arg) => {}] as const);

Copy link
Contributor

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:

function test(arg: { 1: (arg: number) => void; [k: number]: (...args: never) => void }) {}
test([() => {}, (arg) => {}] as const);

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

Copy link
Author

@fwolff fwolff Dec 3, 2024

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:

function test(arg: { 1: (arg: number) => void; foo?: string }) {}
test([null, (arg) => {}] as const);

I only discard types (prior to the discriminateTypeByDiscriminableItems call) if:

  • They are true tuple types with an incompatible length,
  • They are types with at least a non optional property key of the integer form which is out of range (negative or >= of the array value length).

So, now, this is ok:

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);

Two questions:

  1. I use /^[-+]?\d+$/ to match index like property keys (so { "-1": boolean } is discarded because it has a negative index). Is it correct?

  2. { 1: (arg: number) => void } | [1, (arg: boolean) => void] leads to an error with [1, (arg) => { }] (Parameter 'arg' implicitly has an 'any' type), because discriminateTypeByDiscriminableItems fails to discard the first type of the union. I should fix that as well, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use /^[-+]?\d+$/ to match index like property keys (so { "-1": boolean } is discarded because it has a negative index). Is it correct?

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 😅

{ 1: (arg: number) => void } | [1, (arg: boolean) => void] leads to an error with [1, (arg) => { }] (Parameter 'arg' implicitly has an 'any' type), because discriminateTypeByDiscriminableItems fails to discard the first type of the union. I should fix that as well, no?

No, this is correct. Both of those types can provide the type for (arg) => {}. So the implicit any is the correct result here.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thiiink those are mostly redundant. An algorithm matching closer discriminateContextualTypeByObjectMembers would likely suffice here. You could create a list of potential discriminators based on the leading non-spread elements and pass that to discriminateTypeByDiscriminableItems. Then you might apply one extra filter after it (or before it?) to handle tuple-specific cases based on .fixedLength and similar


const elementsLength = node.elements.length;
Copy link
Contributor

Choose a reason for hiding this comment

The 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 getContextualTypeForElementExpression and how it's using firstSpreadIndex and lastSpreadIndex. It feels like you should use a similar-ish logic here but I haven't thought about it deeply. You could also focus on discriminating only by leading non-spread elements first - that should be simpler.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why is contextualType preserved here if everything was filtered out?

Copy link
Author

Choose a reason for hiding this comment

The 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) => {
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 {
Expand All @@ -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;
}
}

Expand Down
114 changes: 114 additions & 0 deletions tests/baselines/reference/tupleTypeInference3.errors.txt
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.
123 changes: 123 additions & 0 deletions tests/baselines/reference/tupleTypeInference3.js
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
Loading
Loading