fix(lib): preserve readonly narrowing in Array.isArray type guard#63609
fix(lib): preserve readonly narrowing in Array.isArray type guard#63609daishuge wants to merge 1 commit into
Conversation
Adds an overload to ArrayConstructor.isArray that narrows readonly array types correctly, fixing a 9-year-old issue where Array.isArray() would lose readonly type information. Fixes microsoft#17002 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@microsoft-github-policy-service agree |
|
The TypeScript team hasn't accepted the linked issue #17002. If you can get it accepted, this PR will have a better chance of being reviewed. |
1 similar comment
|
The TypeScript team hasn't accepted the linked issue #17002. If you can get it accepted, this PR will have a better chance of being reviewed. |
There was a problem hiding this comment.
Pull request overview
This PR aims to fix long-standing narrowing behavior for Array.isArray so that when the input is already a readonly array union (e.g. readonly T[] | U), the type guard preserves readonly instead of narrowing to a mutable any[].
Changes:
- Adds an overload to
ArrayConstructor.isArrayinsrc/lib/es5.d.tsintended to preservereadonlynarrowing. - Adds a new compiler test case covering readonly vs mutable array narrowing and the
unknowncase.
Reviewed changes
Copilot reviewed 1 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/lib/es5.d.ts |
Adds a new Array.isArray overload intended to preserve readonly array narrowing. |
tests/cases/compiler/arrayIsArrayReadonlyNarrowing.ts |
Introduces a compiler test case for the intended narrowing behavior. |
| @@ -0,0 +1,26 @@ | |||
| // @strict: true | |||
|
The type in your first overload is function isArray(arg: any): arg is readonly any[] {
return Array.isArray(arg);
}Unfortunately, that causes If you have the time and the patience, read through some of the older comments on this issue. You'll see that this is a deep rabbit hole that doesn't have an easy solution in the current ecosystem. function isArray(arg: readonly any[] | any): arg is readonly any[];
function isArray(arg: any): arg is any[]; // This overload is never chosen.
function isArray(arg: any) {
return isArray(arg);
}
function any(arg: any): void {
if (isArray(arg)) {
arg.push({}); // TS2339: Property 'push' does not exist on type 'readonly any[]'.
}
}
function unknown(arg: unknown): void {
if (isArray(arg)) {
arg.push({}); // TS2339: Property 'push' does not exist on type 'readonly any[]'.
}
}
function object(arg: object): void {
if (isArray(arg)) {
arg.push({}); // TS2339: Property 'push' does not exist on type 'readonly any[]'.
}
}
function maybeObjectArray(arg: object | object[]): void {
if (isArray(arg)) {
arg.push({}); // TS2339: Property 'push' does not exist on type 'readonly any[] | object[]'.
}
} |
|
+1 to the above comment. The difficulties here have been discussed at length and the lack of a solution is not due to a lack of trying. |
@
Fixes #17002
Problem
Array.isArray()narrows toarg is any[], which silently drops thereadonlymodifier when the input type already carries it:This has been open for 9 years with significant community interest (64 comments).
Fix
One additive overload in
src/lib/es5.d.ts:Overload resolution picks the first matching signature, so:
readonly string[] | stringreadonly string[]string[] | stringstring[]unknownany[]Backward compatibility
A concern raised in #17002 (by @sisp) is that changing
isArrayto returnreadonly unknown[]forunknowninputs would be a breaking change. This PR does not have that problem — the new overload only matches when the input type is alreadyreadonly any[] | T. Forunknown,any, or any mutable array type, overload resolution falls through to the existingisArray(arg: any): arg is any[]signature, so behavior is identical to today.Scope
src/lib/es5.d.ts(additive overload)tests/cases/compiler/arrayIsArrayReadonlyNarrowing.tsreadonlywhen it was already thereSubmitted as a draft to get early feedback on whether this overload shape is the right approach, or whether the team prefers handling this differently (e.g. conditional type, checker change).
@