Repository navigation
Regression in conditional type predicate #51242
Description
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.
Activity
RyanCavanaugh commentedon Oct 20, 2022
Putting up a repro for the bisect bot
RyanCavanaugh commentedon Oct 20, 2022
I believe the root cause here is that the implicit constraint of unconstrained generics behaves closer to
{ } | null | undefinedthanunknown, but don't have a good way to demonstrate that.typescript-bot commentedon Oct 20, 2022
The change between release-4.5 and main occurred at 2c68ded.
RyanCavanaugh commentedon Oct 27, 2022
Absent a strong argument for any particular thing being a defect, I think the right answer here is just that
unknownas the implicit default constraint isn't quite the right mental model, but rather its almost-synonym{ } | null | undefinedgraphemecluster commentedon Oct 28, 2022
Ryan Cavanaugh (@RyanCavanaugh) Not sure if I interpret anything wrong, but writing
T extends {} | null | undefineddoesn’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 errorisArraycould have been dealt a lot easier – that PR would (very likely) have been a solution in TypeScript 4.7.))How about the cases with
Extractexpanded out? Could you please explain why it causes additional errors?github-actions commentedon 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.