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
Array.isArray type narrows to any[] for ReadonlyArray<T> #17002
Comments
|
If you add the following declaration to overload the declaration of interface ArrayConstructor {
isArray(arg: ReadonlyArray<any> | any): arg is ReadonlyArray<any>
}the post-checked 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 |
|
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 |
|
@vidartf As |
|
@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 |
|
What is the best workaround here ? 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 |
|
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 Proposed addition to built-in TS libraries: interface ArrayConstructor {
isArray(arg: ReadonlyArray<any> | any): arg is ReadonlyArray<any>
} |
|
Any news here? :| |
|
I do not understand why we cannot accept the solution from @kambing86: Why do we need to use Really this issue is open since 2017 and seems to be very simple, people even provided ready solutions, just implement. But still type defs for isArray remains the same: |
|
+1 I encountered this problem today with code like this: // The error goes away if you delete "readonly":
type T = readonly string[] | { color: 'red' }
function f(t:T): void {
if (!Array.isArray(t)) {
// Property 'color' does not exist on type 'readonly string[]'
console.log(t.color);
}
}The above code compiles without errors with @valerii15298's solution. Barring some unforeseen consequence, this seems like a one-line fix in lib.es5.d.ts. |
I have incredibly bad news... |
|
The suggested change leads to a backwards incompatibility and (I think) inconsistency though: function f(v: unknown) {
if (Array.isArray(v)) {
// BEFORE:
// `v` is `any[]`
//
// AFTER:
// `v` is `unknown[] | readonly unknown[]`
}
}Currently the narrowed type is a mutable array and the suggested change would make it either mutable or immutable when the variable type is But I agree that |
|
@RyanCavanaugh Would you try this PR? #48228 I modified #42316 (comment) with the following overloads, to fix this issue and #33700, without changing the current behavior of isArray<T>(arg: ArrayLike<T>): arg is readonly T[];
isArray<T>(arg: Iterable<T>): arg is readonly T[]; |
export type ITSArrayListMaybeReadonly<T> = T[] | readonly T[];
declare global
{
interface ArrayConstructor
{
isArray<T extends ITSArrayListMaybeReadonly<any>>(arg: T | unknown): arg is T
}
}
export function isArray<T extends ITSArrayListMaybeReadonly<any>>(value: T | unknown): value is T
{
return Array.isArray(value)
} |
let a: string[] | number = 42
if (Array.isArray(a)) {
console.info(a)
// let a: number & any[]
}
a = []It's really wired that we got a ts(2358) // ERROR: The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter.ts(2358)
if (a instanceof Array) {
// ...
} |
|
That's just control flow analysis; the type of Hmm, you keep editing that comment. Let me know when it stabilizes. |
|
@jcalz Thanks for pointing the control flow analysis to me! After a few tests, I started understanding the
I'm still trying to test its behavior (you see my modification in the above issue), I feels that:
Here's a similar post: Question about Array.isArray() and unknown[] ConclusionMy 2 cents will be:
|
|
I couldn't get the above solutions to work for my use case so I made a local isArray function. Here it is incase others find it helpful: export function isArray(
arg: ReadonlyArray<any> | any
): arg is ReadonlyArray<any>;
export function isArray(arg: any): arg is any[] {
return Array.isArray(arg);
} |
|
@RyanCavanaugh please bring some attention to this issue |
|
I think I've got a solution that extracts the relevant array type without breaking the In both cases, they're built upon the backs of the many giants who fell down this rabbit hole before me. First, the breaking solution: type IfUnknownOrAny<T, Y, N> = unknown extends T ? Y : N;
type ArrayType<T> = IfUnknownOrAny<
T,
T[] extends T ? T[] : any[] & T,
Extract<T, readonly any[]>
>;
declare global {
interface ArrayConstructor {
isArray<T>(arg: T): arg is ArrayType<T>;
}
}The breaking change here is that if your starting type is But the behavior of the current official library does result in giving you an type IfUnknownOrAny<T, Y, N> = unknown extends T ? Y : N;
type IfAny<T, Y, N> = (T extends never ? 1 : 0) extends 0 ? N : Y;
type ArrayType<T> = IfUnknownOrAny<
T,
IfAny<T, T[] extends T ? T[] : T[] & T, any[] & T>,
Extract<T, readonly any[]>
>;
declare global {
interface ArrayConstructor {
isArray<T>(arg: T): arg is ArrayType<T>;
}
}Static analysis confirms the types I would expect for everything I've thus far tested. First, we have some tests showing the current behavior, which doesn't result in selecting the preferred type in some cases (primarily when the input type could be a read-only array). Next, we have the first version above, which opts for the more correct Finally, we have the compatible version that will still produce |
|
I've found an oversight in my const object: object = {};
if (Array.isArray(object))
typeOf(object).is<never>("🟢 true"); // 😞So I tried a simplified version of @graphemecluster's implementation: type ArrayType<T> = Extract<
true extends false & T ?
any[] :
T extends readonly any[] ?
T :
unknown[],
T
>;This one works correctly for all my previously tested cases, plus ones involving Note that, like my first version, it converts I also made a more compact, less explicit version of these tests. |
|
@P-Daddy I would like to see |
|
@RyanCavanaugh @sandersn Suppose my solution is still not feasible and convincing enough (at least from TypeScript 4.8), has the Team ever considered using an intrinsic type? I guess it must be one of the most probable solutions for this long-standing issue. |
|
I'll bring it to the design meeting next week. This is incredibly subtle, unfortunately. |

TypeScript Version: 2.4.1
Code
Expected behavior: Should type narrow to
ReadonlyArray<T>, or at the very leastT[].Actual behavior: Narrows to
any[]. Doesn't trigger warnings in noImplicitAny mode either.The text was updated successfully, but these errors were encountered: