-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Fixed issues with inference from nested tuple types of the same shape #49226
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
Fixed issues with inference from nested tuple types of the same shape #49226
Conversation
…sted tuple types of the same shape
src/compiler/checker.ts
Outdated
@@ -20929,8 +20929,7 @@ namespace ts { | |||
return type.symbol; | |||
} | |||
if (isTupleType(type)) { | |||
// Tuple types are tracked through their target type | |||
return type.target; | |||
return type; |
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'm not overly confident that it's the correct shape of this fix but all tests pass.
The problem was that for the inner tuple the same recursion identity was computed here as the type.target
was shared between those tuples (as they have the same shape). And thus invokeOnce
was skipping over action(source, target)
call there while inferring for the inner tuple.
This check is done within this if statement:
https://github.dev/microsoft/TypeScript/blob/942d2a91c115f22857513a9d3f4bde2fb1521f50/src/compiler/checker.ts#L20918-L20919
However, this still goes into this if statement for a tuple because it doesn't satisfy the isObjectOrArrayLiteralType(type)
check. I think that perhaps a better fix here would be to make a change to this if condition. I've tried this alternative patch and all tests seem to be passing to:
Alternative patch
diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 7bcc6f5bdc..efce2901b4 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -20916,7 +20916,7 @@ namespace ts {
// reference the type have a recursion identity that differs from the object identity.
function getRecursionIdentity(type: Type): object {
// Object and array literals are known not to contain recursive references and don't need a recursion identity.
- if (type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) {
+ if (type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type) && !isTupleType(type)) {
if (getObjectFlags(type) && ObjectFlags.Reference && (type as TypeReference).node) {
// Deferred type references are tracked through their associated AST node. This gives us finer
// granularity than using their associated target because each manifest type reference has a
@@ -20928,10 +20928,6 @@ namespace ts {
// exclude the static side of classes from this check since it shares its symbol with the instance side.
return type.symbol;
}
- if (isTupleType(type)) {
- // Tuple types are tracked through their target type
- return type.target;
- }
}
if (type.flags & TypeFlags.TypeParameter) {
return type.symbol;
The comment above the isTupleType
suggests that it's checking if the type was written as literal tuple type:
https://github.dev/microsoft/TypeScript/blob/942d2a91c115f22857513a9d3f4bde2fb1521f50/src/compiler/checker.ts#L21278-L21282
and the comment above the mentioned condition suggests that we don't want to go into this block if a type was written as literal (it only mentions arrays and objects though):
https://github.dev/microsoft/TypeScript/blob/942d2a91c115f22857513a9d3f4bde2fb1521f50/src/compiler/checker.ts#L20918
So perhaps this isTupleType
check shouldn't be inside this if block from the start?
❤️ |
@ahejlsberg LGTY? This has shown up with a few manifestations |
I was thinking about it more and I think it's the correct change semantically. A tuple can't really reference itself directly as far as I know - so it always has to go through an alias. If it goes through an alias then that alias won't be entered again thanks to the recursion identity check and the tuple in it won't even be reached. Would there be a chance for a playground build of this PR? |
@typescript-bot pack this |
Heya @jakebailey, I've started to run the tarball bundle task on this PR at a7d5826. You can monitor the build here. |
Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
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.
Honestly, the logic presented here, that tuple structure can only be self-recursive in the presence of aliases, makes sense to me, and makes using the tuple type itself as the recursion identity seem fine (since somewhere in the stack will be a repeating alias symbol).
Just noticed this was merged, but it causes an issue in this example: type T1<T> = [number, T1<{ x: T }>];
type T2<T> = [42, T2<{ x: T }>];
function qq<U>(x: T1<U>, y: T2<U>) {
x = y; // Excessive stack depth comparing types 'T2<U>' and 'T1<U>'
y = x; // Error as expected
} The change in the PR effectively disables |
@ahejlsberg do u have a suggested fix for this in mind or should I investigate this? |
You could probably change it from |
That doesn't seem to cut it. I'm gonna investigate why sometime later today. |
Ah, probably because we call |
I have a fix for this. We solved a similar problem in function invokeOnce<Source extends Type, Target extends Type>(source: Source, target: Target, action: (source: Source, target: Target) => void) {
const key = source.id + "," + target.id;
const status = visited && visited.get(key);
if (status !== undefined) {
inferencePriority = Math.min(inferencePriority, status);
return;
}
(visited || (visited = new Map<string, number>())).set(key, InferencePriority.Circularity);
const saveInferencePriority = inferencePriority;
inferencePriority = InferencePriority.MaxValue;
// We stop inferring and report a circularity if we encounter duplicate recursion identities on both
// the source side and the target side.
const saveExpandingFlags = expandingFlags;
(sourceStack ??= []).push(source);
(targetStack ??= []).push(target);
if (isDeeplyNestedType(source, sourceStack, sourceStack.length, 2)) expandingFlags |= ExpandingFlags.Source;
if (isDeeplyNestedType(target, targetStack, targetStack.length, 2)) expandingFlags |= ExpandingFlags.Target;
if (expandingFlags !== ExpandingFlags.Both) {
action(source, target);
}
else {
inferencePriority = InferencePriority.Circularity;
}
targetStack.pop();
sourceStack.pop();
expandingFlags = saveExpandingFlags;
visited.set(key, inferencePriority);
inferencePriority = Math.min(inferencePriority, saveInferencePriority);
} Plus change the declarations of Best I can tell this takes care of the problem. |
I can put up a PR with these changes. Alternatively, @Andarist you can put up a new PR. |
@ahejlsberg if u already have the fix then go for it :) |
fixes #48524
fixes #52722