TypeScript Version: 2.4.1
Code
declare interface T {
foo: string;
}
const immutable: T | ReadonlyArray<T> = [];
if (Array.isArray(immutable)) {
const x = immutable; // Any[] - Should be ReadonlyArray<T>
}
const mutable: T | Array<T> = [];
if (Array.isArray(mutable)) {
const x = mutable; // T[]
}
Expected behavior: Should type narrow to ReadonlyArray<T>
, or at the very least T[]
.
Actual behavior: Narrows to any[]
. Doesn't trigger warnings in noImplicitAny mode either.
If you add the following declaration to overload the declaration of isArray()
:
interface ArrayConstructor {
isArray(arg: ReadonlyArray<any> | any): arg is ReadonlyArray<any>
}
the post-checked mutable
is still narrowed to T[]
and the post-checked immutable
is narrowed to ReadonlyArray<T>
as desired.
I'm not sure if this or similar would be an acceptable addition to the standard typing libraries or not, but you can at least use it yourself.
@jcalz yes, I think these overloads should be in the standard library. There are a few other scenarios where ReadonlyArray isn't accepted where it should be (for example Array.concat
).
The same narrowing problem also exists for this check:
if (immutable instanceof Array) {
const x = immutable; // Any[] - Should be ReadonlyArray<T>
}
I'm not sure if there exists a similar workaround for this.
I would probably do this if I wanted a workaround:
interface ReadonlyArrayConstructor {
new(arrayLength?: number): ReadonlyArray<any>;
new <T>(arrayLength: number): ReadonlyArray<T>;
new <T>(...items: T[]): ReadonlyArray<T>;
(arrayLength?: number): ReadonlyArray<any>;
<T>(arrayLength: number): ReadonlyArray<T>;
<T>(...items: T[]): ReadonlyArray<T>;
isArray(arg: any): arg is ReadonlyArray<any>;
readonly prototype: ReadonlyArray<any>;
}
const ReadonlyArray = Array as ReadonlyArrayConstructor;
And then later
if (ReadonlyArray.isArray(immutable)) {
const x = immutable; // ReadonlyArray<T>
}
if (immutable instanceof ReadonlyArray) {
const x = immutable; // ReadonlyArray<T>
}
but of course, since at runtime there's no way to tell the difference between ReadonlyArray
and Array
, you would have to be careful to use the right one in the right places in your code. ¯\_(ツ)_/¯
@vidartf As instanceof
works at runtime, I'm not sure that should narrow to ReadonlyArray
. You're asking if the runtime representation of that is an array, which is _true_. So I'd expect it to narrow to Array<T>
, not ReadonlyArray<T>
.
@jinder I didn't state it explicitly, but my code was meant to be based on yours (same variables and types), so it should already know that it was T | ReadonlyArray<T>
. As such, instanceof
should narrow it to ReadonlyArray<T>
.
What is the best workaround here ?
I just ended up with ugly type assertion:
function g(x: number) {}
function f(x: number | ReadonlyArray<number>) {
if (!Array.isArray(x)) {
g(x as number); // :(
}
}
I think this is fixed in 3.0.3.
@adrianheine doesn't seem to be for me.
Oh, yeah, I was expecting the code in the original issue to not compile, bu that's not even the issue.
let command: readonly string[] | string;
let cached_command: Record<string, any>;
if (Array.isArray(command))
{
}
else
{
cached_command[command] = 1;
// => Error: TS2538: Type 'readonly string[]' cannot be used as an index type.
}
Temporary solution from @aleksey-l (on stackoverflow) until the bug is fixed:
declare global {
interface ArrayConstructor {
isArray(arg: ReadonlyArray<any> | any): arg is ReadonlyArray<any>
}
}
It is not only ReadonlyArray: #33700
Here's a concrete example of ReadonlyArray<string>
behaving differently than Array!Array.isArray()
case fails to eliminate the ReadonlyArray<string>
when narrowing.
it should be like this
interface ArrayConstructor {
isArray(arg: unknown): arg is unknown[] | readonly unknown[];
}
and test it in typescript
const a = ['a', 'b', 'c'];
if (Array.isArray(a)) {
console.log(a); // a is string[]
} else {
console.log(a); // a is never
}
const b: readonly string[] = ['1', '2', '3']
if (Array.isArray(b)) {
console.log(b); // b is readonly string[]
} else {
console.log(b); // b is never
}
function c(val: string | string[]) {
if (Array.isArray(val)) {
console.log(val); // val is string[]
}
else {
console.log(val); // val is string
}
}
function d(val: string | readonly string[]) {
if (Array.isArray(val)) {
console.log(val); // val is readonly string[]
}
else {
console.log(val); // val is string
}
}
function e(val: string | string[] | readonly string[]) {
if (Array.isArray(val)) {
console.log(val); // val is string[] | readonly string[]
}
else {
console.log(val); // val is string
}
}
Would a PR that adds the appropriate overload to Array.isArray
be reasonable at this point? This issue is marked as In Discussion, but I'm not sure what discussion is necessary. This just feels like a mistake in the definition files, which should be an easy fix that any contributor can submit.
Proposed addition to built-in TS libraries:
interface ArrayConstructor {
isArray(arg: ReadonlyArray<any> | any): arg is ReadonlyArray<any>
}
Any news here? :|
Removed ReadonlyArray<any> | any
hack
interface ArrayConstructor {
isArray(arg: any[]): arg is any[];
isArray(arg: any): arg is readonly any[];
}
You could also do:
interface ArrayConstructor {
isArray(arg: any): arg is any[] | readonly any[];
}
This will work correctly for union types that contain arrays or readonly arrays, and as long as you don’t mutate the result, will even work for any
or unknown
.
It widens readonly any[]
to any[] | readonly any[]
.
I'm not sure if this is the same root cause but I'm seeing this now with readonly tuples.
Most helpful comment
Would a PR that adds the appropriate overload to
Array.isArray
be reasonable at this point? This issue is marked as In Discussion, but I'm not sure what discussion is necessary. This just feels like a mistake in the definition files, which should be an easy fix that any contributor can submit.Proposed addition to built-in TS libraries: