2

I'm just wondering why the type inference for the intellisense gets lost inside an Array.isArray condition.

Consider the following snippet:

type T = {
    readonly name: string;
    readonly descr: string;
}

interface I{
    readonly tags: ReadonlyArray<T>;
}

function Z(arg: I): void{
    const { tags } = arg;
    if (Array.isArray(tags)) {  //hovering "tags" here shows "readonly T[]"
        for (let t of tags) {   //hovering "tags" here shows "any[]"

        }
    }
}

Z({
    tags:[]
})

In other words, why the original type is not preserved from its declaration, and changes getting the isArray signature instead?

Tested on Visual Studio, and also in the playground.

2 Answers 2

3

It is a known issue with ReadonlyArray and the type guard function Array.isArray. You can also find a possible fix here.

In other words, why the original type is not preserved from its declaration, and changes getting the isArray signature instead?

Part of the reason is, that Array<any> actually is a sub-type of ReadonlyArray<any>.

type IsArraySubtypeOfROArray = Array<any> extends ReadonlyArray<any> ? true : false // true
type IsROArraySubtypeOfArray = ReadonlyArray<any> extends Array<any> ? true : false // false

As Maciej says in his answer, there is no check necessary, as you can already be sure to have an array in above case. So let's assume property tags has type T | ReadonlyArray<T> to make it more interesting.

With the built-in ArrayConstructor interface

interface ArrayConstructor {
  ...
  isArray(arg: any): arg is any[];
}

and given Array.isArray(tags) returns true, the compiler compares type T | ReadonlyArray<T> from tags with type any[] from isArray return type. Neither T nor ReadonlyArray<T> are a sub type of any[]. So the compiler looks the other way round, if any[] is a sub-type of T or ReadonlyArray<T>. As that is the case with ReadonlyArray<T>, control flow analysis now resolves to any[] as the most narrow type possible.

Here is a playground with a properly typed example.

Sign up to request clarification or add additional context in comments.

1 Comment

I moved the best answer here. As said, my real code is much more complex than the snippet, and I can't rely on a simple declaration because the data comes from another source (that is, the field may be even undefined). I typically use isArray when I have an optional parameter which expects an array.
0

The problem is in

Array.isArray(tags)

This type guard if passed is stating that you work with any[]. This is quite understandable us if you checking if something is an array. This guard doesnt check the element, and it is hard to check as array can be empty. The standard case of using Array.isArray is situation where you don't know what you got (like union type where value can or can be not an array), and you want to ensure that it is an array. Next thing is to check array of what.

Your example is quite unusual as there is no need for check, as you have statically set that you get T[] as an input. So for type system Array.isArray(tags) has no sense as tags are known to be an array of T.

You can fix this behavior by custom type guard:

const isArray = <V>(a: any): a is V[] => Array.isArray(a);
// above guard set additional type information to the array element

// usage
if (isArray<T>(tags)) {  //hovering "tags" here shows "readonly T[]"
        tags // here tags are T[]
    }

But in your example this has no sense until the input will be something less specific then just T[], for example it would have sense for input like T | T[]:

interface I{
    readonly tags: ReadonlyArray<T> | T; // union type
}

Then checking has a sense as we have branching here to deal differently with T[] and T.

2 Comments

My example is a cut of a larger piece of code. The "tags" field is yes part of an interface, but the actual object could come from "anywhere", hence no guarantee that it'll be an array. However, if it is an array, it is made of that T. Thank you, anyway: I missed this perspective.
I'm sorry, but I find the ford04 answer more exaustive, thus I've given him the reward. I thank you again for your noticeable explaination.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.