Skip to content
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

Regression in conditional type predicate #51242

Closed
graphemecluster opened this issue Oct 20, 2022 · 6 comments
Closed

Regression in conditional type predicate #51242

graphemecluster opened this issue Oct 20, 2022 · 6 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@graphemecluster
Copy link
Contributor

Bug Report

Search Terms: type assertion, type comparison, related to, generic parameter

Version & Regression Information: This changed between versions 4.7 and 4.8

Code: Playground Link

I discovered this issue in my PR #50454 when I am looking for a possible solution to the typing of Array.isArray.
Consider the following code:

declare function f0<T>(arg: T): arg is Extract<T, readonly any[]>;
declare function f1<T>(arg: T): arg is Extract<T, any[]>;
declare function f2<T>(arg: T): arg is {} extends T ? T & any[] : Extract<T, readonly any[]>;
declare function f3<T>(arg: T): arg is {} extends T ? T & any[] : Extract<T, any[]>;
declare function f4<T>(arg: T): arg is T extends any ? {} extends T ? T & any[] : Extract<T, readonly any[]> : never;
declare function f5<T>(arg: T): arg is T extends any ? {} extends T ? T & any[] : Extract<T, any[]> : never;
declare function f6<T>(arg: T): arg is T extends any ? Extract<T, readonly any[]> : never;
declare function f7<T>(arg: T): arg is T extends any ? Extract<T, any[]> : never;
declare function f8<T>(arg: T): arg is T extends readonly any[] ? T : never;
declare function f9<T>(arg: T): arg is T extends any[] ? T : never;
declare function fa<T>(arg: T): arg is {} extends T ? T & any[] : T extends readonly any[] ? T : never;
declare function fb<T>(arg: T): arg is {} extends T ? T & any[] : T extends any[] ? T : never;
declare function fc<T>(arg: T): arg is T extends any ? {} extends T ? T & any[] : T extends readonly any[] ? T : never : never;
declare function fd<T>(arg: T): arg is T extends any ? {} extends T ? T & any[] : T extends any[] ? T : never : never;
declare function fe<T>(arg: T): arg is T extends any ? T extends readonly any[] ? T : never : never;
declare function ff<T>(arg: T): arg is T extends any ? T extends any[] ? T : never : never;

function case1<T extends any>(a: T | T[]): T[] {
	return f(a) ? a : [a];
}
function case2<T extends unknown>(a: T | T[]): T[] {
	return f(a) ? a : [a];
}
function case3<T>(a: T | T[]): T[] {
	return f(a) ? a : [a];
}

In the above, f4 is the simplified version of the type I used in the PR.

Actual behavior:

From 4.8, the results vary from function to function. In case 3, an error occurs when f is f4 but not in case 1 and case 2 or the case when f is f2 where T is not distributed using T extends any.
Here is a table of whether a ts2322 error occurs in each cases:

f Case 1 Case 2 Case 3
f0 🛑 🛑 🛑
f1
f2
f3
f4 🛑
f5
f6 🛑 🛑 🛑
f7

For the latter half, f(n+8) is simply fn with Extract expanded out, but I could hardly understand why the behavior changes:

f Case 1 Case 2 Case 3
f8 🛑 🛑 🛑
f9
fa
fb
fc 🛑
fd 🛑
fe 🛑 🛑 🛑
ff 🛑 🛑 🛑

(Tested in v4.9.0-dev.20221020 and 4.8.4)

Prior to 4.8, ALL the cases passed:

f Case 1 Case 2 Case 3
f0
f1
f2
f3
f4
f5
f6
f7
f8
f9
fa
fb
fc
fd
fe
ff

(Tested in 4.7.4 and 4.6.4)

(Note: some of the test functions above are for control (i.e. comparison) only.)

Expected behavior:

For each row, the results of case 1, case 2 and case 3 should be identical. (f4, fc, fd)
For each column, the results of fn and f(n+8) should be identical. (f5, fd, f7, ff)

I understand that some of them must be desirable changes, so I am not expecting the results to be the same between 4.7 and 4.8, but the mismatched results due to the absent of generic constraint are really weird.

@RyanCavanaugh
Copy link
Member

Putting up a repro for the bisect bot

declare function f4<T>(arg: T): arg is T extends any ? {} extends T ? T & any[] : Extract<T, readonly any[]> : never;

function case1<T extends any>(a: T | T[]): T[] {
	return f4(a) ? a : [a];
}
function case2<T extends unknown>(a: T | T[]): T[] {
	return f4(a) ? a : [a];
}
function case3<T>(a: T | T[]): T[] {
	return f4(a) ? a : [a];
}

@RyanCavanaugh
Copy link
Member

I believe the root cause here is that the implicit constraint of unconstrained generics behaves closer to { } | null | undefined than unknown, but don't have a good way to demonstrate that.

@typescript-bot
Copy link
Collaborator

The change between release-4.5 and main occurred at 2c68ded.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Oct 27, 2022
@RyanCavanaugh
Copy link
Member

Absent a strong argument for any particular thing being a defect, I think the right answer here is just that unknown as the implicit default constraint isn't quite the right mental model, but rather its almost-synonym { } | null | undefined

@graphemecluster
Copy link
Contributor Author

@RyanCavanaugh Not sure if I interpret anything wrong, but writing T extends {} | null | undefined doesn’t give an error either (in fact extending anything silence the error – leaving #50454 aside, I think you’d like to make it raise errors (though without the error isArray could have been dealt a lot easier – that PR would (very likely) have been a solution in TypeScript 4.7.))

How about the cases with Extract expanded out? Could you please explain why it causes additional errors?

@github-actions
Copy link

github-actions bot commented Jun 8, 2023

This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

3 participants