Repository: TypeScript
Issue: Array.isArray() : a possible fix ? + discussion on type guards (frozen keyword + better type deduction)
Labels: Suggestion,Awaiting More Feedback

### 🔍 Search Terms

in:title frozen
in:title freeze

+ I don't remember for Array.isArray()

I searched for issues on `Array.isArray()` and found a *lot* of them, too much to list them all.
3 weeks ago I suggested something that could lead to a possible fix [on an existing issue](https://github.com/microsoft/TypeScript/issues/17002#issuecomment-2366749050).

### ✅ Viability Checklist

- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- [x] This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

### ⭐ Suggestion

The type guard for `Array.isArray()` is currently erroneous and the fix potentially quite complex.
In retrospect I think the potential fix I suggested previously is more a "workaround" (still, you can get a look at it), and that there are issues on type guards.

I'd like to discuss here what should be the behavior of `Array.isArray()` on different cases, and to discuss possibilities to simplify the current "workaround" by improving type guards behavior.

Summary :
- a `frozen` keywords putting `never` to some properties (alternatively could also be a `frozen` state e.g. to some functions, preventing their call/making them non-callable) to act as a constraint as opposed to `readonly` which is only a partial interface. This could help solving the current issue of using `readonly` as a constraint, which generate lot of unsoundness + would solve use of `readonly` arrays with `Array.isArray()`.
- better deducing generic types could also improve type guards.


First, for the sake of simplicity, let's assume :
```ts
function isArray(a: unknown): a is unknown[];

type A = typeof a;
Is   <A, unknown[]> // the type of a if isArray() is TRUE
IsNot<A, unknown[]> // the type of a if isArray() is FALSE
```

In the general case :
```ts
Is   <T, U> = T&U;
IsNot<T, U> = Exclude<T,U>;
```

Of course, type guards need to do more than that, and are currently doing more, but not enough.

**Union:**

```ts
Is<T1|T2, U> = Is<T1, U> | Is<T2, U>;
```

Currently, Type guards and `&` seem to behave as expected.
Currently, `(T1|T1)&U` is distributed as ` (T1&U) | (T2&U)` to remove some `never` then factorized when the type is printed.

**Child class :**

```ts
Is<T extends U, U> = T;
```

Currently, Type guards behave as expected, **but** `&` doesn't (but not an issue).
```ts
class A<T> extends Array<T> { /* ... */ }

type T = A<any> & Array<any>; // A<any> expected, got A<any> & any[].
```

**Base type**

```ts
Is<T, U extends T> = U;
```


Currently, Type guards behave as expected, **but** `&` doesn't (but not an issue).
```ts
interface A {
        get length(): number;
    }

type C = A & Array<any>; // expected Array<any>, got Array<any> & A.
```

**Readonly:**

There is 2 ways to see `readonly` :
- as a constraint : `readonly T & T = readonly T`.
- as a partial interface : `readonly T & T = T`.

For TS, it is saw as a partial interface, therefore : `readonly T & T = T`.
Which is quite confusing as, in practice, we mainly use it as a constraint, but this is a design choice, why not.

The issue is that the type of ̀`Object.freeze([])` is also a `readonly []`, when this is not a partial interface, BUT a **constraint**.
This is an inconsistency in the design, which could be solved with e.g. a `frozen` keyword : `frozen number[]` which would set some properties/methods as `never` instead of simply removing them :
```ts
    type A = {
        a: 3,
        b: 4
    }

    type Excl<T, keys> = {
        [K in keyof T]: K extends keys ? never : T[K]
    }

    type B = Excl<A, "b">;
   // or type B = number [] & { push: never };

    type C = B&A;
    let c = f<C>();
    c.b // never
```

Currently, `Array.isArray(readonly T[])`, assert `T` as being `any[]`, which is wrong for 2 reasons :
1. the generic type information is lost.
2. the `readonly` information is lost.

I argue that as `Array.isArray(Object.freeze([]))` returns true, so we shouldn't remove the `readonly` keyword.
But should it be at the type guard level, or at the `Array.isArray()` call signature level ?

On one side `readonly` is only a partial interface, and on the other side, it is often used as a constraint (a `frozen` keyword would solve this).
On another side, `readonly` is at the type level, when the type guard function is based on the value during execution.

Therefore, without `frozen` there is 4 solutions :
1. Add `readonly` at the `Array.isArray()` level, and require other devs to do so for their type guards.
2. Handle `readonly` at the type guard level, with `Is<readonly T, U> = readonly (T&U)`, which would be ambiguous as readonly isn't a constraint, but a partial interface.
3. In type guards, if `T extends readonly U`, makes `U` implicitly `readonly`. `Is<T extends readonly U, U> = T & readonly U` otherwise `Is<T,U> = T&U`, which might also be confusing.
4. Assume that type guards offer information on the runtime value, but not on the desired TS type, i.e. : `Is<T, U extends T> = T` and requires an explicit cast to get a `U` (which would always be legit), which would be horrible.


And here the good stuff, with generics...

**Base type + generics **

```ts
    interface A {
        push(...args: number[]): void;
        pop(): number|undefined;
    }

    type X = Array<number> extends A ? true : false; // true
    let a = f<A>();

    if(Array.isArray(a) )
        a; // any[] <- should be number[]
```

We should try to assert the generic types :
```ts
// with U<number> extends T;
Is<T, U<unknown>> = Is<Partial<U<number>>, U<unknown>> = U<unknown&number> = U<number>; 
```

I think a type deduction is technically possible in lot of cases, an would simplify lot of type guards using generics.
The issue is to assert when the following step would be legal in a type guard :
```ts
Is<Partial<U<number>>, U<unknown>> = U<unknown&number>
```
Maybe if, and only if, `U<unknown&number> extends U<unknown>` ?

We could even be more generic :
```ts
// with U<number> extends Pick<T, keyof U>;
Is<T, U<unknown>> = Is<Partial<U<number>>, U<unknown>> & T = U<unknown&number> & T = U<number> & T; 
```

When we can't deduce, I suggest:
- `Is<unknown, U<V>> = U<V>`
- `Is<any, U<V>>            = U<V>`, but `U<any>` if `V = unknown`.
- `Is<{},  U<V>>              = U<V>`.



This issue also occurs with `readonly unknown[]`, as it can be seen as a base type of `unknown[]`.

### 📃 Motivating Example

This would lead to more precise type deduction in type guards.

### 💻 Use Cases

1. What do you want to use this for?

Deduce type more precise types.

2. What shortcomings exist with current approaches?

Deduced types are incorrect/not precised.

3. What workarounds are you using in the meantime?

Complex type guards.