-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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'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. |
@RyanCavanaugh Just wondering whether this was brought up on the design meeting and whether any decision was made? |
This is one of the more frustrating bugs to keep running into when dealing with readonly arrays. cc @DanielRosenwasser is this something we can bring back to the table? I understand that it's not going to be a simple problem to solve broadly - but I think there's a lot of value in making this work broadly - there's a lot of usecases where people opt for a mutable array instead of a readonly array purely because you can't narrow the type. It looks like internally TS itself is bitten by this problem, leading you guys to define your own internal signature to work around it: TypeScript/src/compiler/core.ts Lines 1833 to 1841 in f218a56
|
Thanks, @bradzacher, for pinging this. I'd also love to see this addressed. Just to underscore how nuanced it is, and sympathize with the TypeScript team for not yet providing a solution, here's a situation I ran into just yesterday, in which none of the signatures I tried for type Args<A extends readonly any[]> = A | {args: A};
function foo<A extends readonly any[]>(args: Args<A>): readonly any[] {
if (Array.isArray(args))
return args;
return args.args;
} Playground link with one example signature that fails. For the curious, I worked around it with the signature below, which handles this particular case, but fails on a number of other ones. Because of that, I just created a local override of const isArray = Array.isArray as <T extends readonly any[]>(obj: unknown) => obj is T; |
Hi everyone, I was having some trouble overriding the Thanks to @P-Daddy and @graphemecluster comments, here's the file I came up with:
I save it in a declaration file and this resolves all my guards without breaking other functions. Hope this can help someone ! |
How about:
Simpler and works for me. |
That would mean |
Correct! |
It seems like |
@robertherber I believe that doesn't work across windows and the like (eg iframes) (which is the reason that |
That is correct:
also:
There are good reasons why |
Question : would the following code help to solve this issue (I passed all test I could think of)? type X<T> = Exclude<unknown,T> extends never ? T[] : T extends readonly unknown[] ? T : never;
/*
A type predicate's type must be assignable to its parameter's type.
Type 'X<T>' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'X<T>'.
=> @ts-ignore ?
*/
function isArray<T>(a: T): a is X<T> {
return Array.isArray(a);
} let h = 4;
if( isArray(h) )
h; // never
else
h; // number
let a = {}
if( isArray(a) )
a; // never
else
a; // {}
let b = new Array<number>();
if( isArray(b) )
b // number[]
else
b // never
let d = Array<number>() as readonly number[];
if( isArray(d) )
d // readonly number[]
else
d // never
let e = Array<number>() as number[]|readonly number[];
if( isArray(e) )
e // number[] | readonly number[]
else
e // never
function foo(): number|number[] {
throw 'e';
}
let c = foo();
if( isArray(c) )
c // number[]
else
c // number
let t = foo() as unknown;
if( isArray(t) )
t // unknown[]
else
t // unknown
// doesn't work
let g = null as any;
if( isArray(g) )
g; // any[]
else
g; // any
let f: never = null as unknown as never;
if( isArray(f) )
f; // never
else
f; // never
let j = [] as never[];
if( isArray(j) )
j; // never[]
else
j; // never
let i = [] as unknown[];
if( isArray(i) )
i; // unknown[]
else
i; // never
let k = [] as any[];
if( isArray(k) )
k; // any[]
else
k; // never EDIT1: fix issue when type X<T> = Exclude<unknown,T> extends never ? T[] : T extends readonly unknown[] ? T : never;
type X2<T> = Exclude<unknown,T> extends never ? unknown : T;
function isArray<T>(a: T|X2<T>): a is X<T> {
return Array.isArray(a);
} |
New version :
Do I forget some cases ? type X<T> = Exclude<unknown,T> extends never ? T[] // any/unknown => any[]/unknown
: T extends readonly unknown[] ? T // unknown[] - obvious case
: T extends Iterable<infer U> ? readonly U[] // Iterable<U> might be an Array<U>
: unknown[] extends T ? unknown[] // something that could be an array - no ways to get the real type ?
: readonly unknown[] extends T ? readonly unknown[] // something that could be an array - no ways to get the real type ?
: any[] extends T ? any[] // something that could be an array - no ways to get the real type ?
: readonly any[] extends T ? readonly any[] // something that could be an array - no ways to get the real type ?
: never; // else never
type X2<T> = Exclude<unknown,T> extends never ? unknown
: unknown; // required for any/unknown + Iterable<U>
function isArray<T>(a: T|X2<T>): a is X<T>
function isArray (a: unknown): boolean {
return Array.isArray(a);
} |
// @ts-expect-error error but works
export const isArray = Array.isArray as <T>(arg: T) => arg is unknown extends T // T is `unknown` or `any`
? unknown[]
: T extends readonly unknown[]
? T
: never |
Try this: export const isArray = Array.isArray as <T>(arg: T) => typeof arg extends (infer K extends unknown)[] // T is `unknown` or `any`
? K[]
: T extends readonly (infer K extends unknown)[]
? T
: never This is probably closer, but not guaranteed. |
I have a kind of test suite in my playground.
|
I think you made a mistake somewhere :
Currently, TS deduce it as On one side, in JS we can do : class B extends Array { valueOf(){ return 4; } } // B can also be viewed as a number.
Array.isArray(new B()); // true Maybe the last |
Update: maybe a cleaner version : type X<T> = Exclude<unknown,T> extends never ? T[] // any/unknown => any[]/unknown[]
: T extends readonly unknown[] ? T // unknown[] - obvious case
: T extends Iterable<infer U> ? readonly U[] // Iterable<U> might be an Array<U>
: unknown[]; // by default
type X2<T> = Exclude<unknown,T> extends never ? unknown
: unknown; // required for any/unknown + Iterable<U>
function isArray<T>(a: T|X2<T>): a is X<T>
function isArray (a: unknown): boolean {
return Array.isArray(a);
} Update2: handling partial types: type PickMatching<V, T> = { [K in keyof T]: (T&V)[K] };
type X<T> = Exclude<unknown,T> extends never ? T[] // any/unknown => any[]/unknown
: T extends readonly unknown[] ? T // unknown[] - obvious case
: T extends PickMatching<Array<infer U>, T> ? readonly U[] // try to deduce the generic argument
// or : T extends PickMatching<Array<infer U>, T> ? U[] // try to deduce the generic argument
: T extends Iterable<infer U> ? readonly U[] // Iterable<U> might be an Array<U>
: unknown[]; // else never
type X2<T> = Exclude<unknown,T> extends never ? unknown
: unknown; // required for any/unknown + Iterable<U>
function isArray<T>(a: T|X2<T>): a is X<T>
function isArray (a: unknown): boolean {
return Array.isArray(a);
} |
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: