total-typescript / ts-reset

A 'CSS reset' for TypeScript, improving types for common JavaScript API's
https://www.totaltypescript.com/ts-reset
MIT License
7.74k stars 117 forks source link

Improvment to `Array.isArray` #151

Open Mickemouse0 opened 1 year ago

Mickemouse0 commented 1 year ago

This PR improves the type inference of Array.isArray.

It builds on the changes made in https://github.com/total-typescript/ts-reset/pull/56 and https://github.com/total-typescript/ts-reset/pull/23 to solve https://github.com/total-typescript/ts-reset/issues/48.

Mickemouse0 commented 1 year ago

@mattpocock @DeepDoge @phenomnomnominal I have not been able to make it pass the final test. If you have any suggestions I'd be happy for the help.

Mickemouse0 commented 1 year ago

I have been able to get either or of

doNotExecute(() => {
  function test<T>(value: T) {
    type Unarray<T> = T extends Array<infer U> ? U : T;
    const inner = <X extends Unarray<T>>(v: X[]) => {};

    if (Array.isArray(value)) {
      inner(value);
    }
  }
});

doNotExecute(async () => {
  function makeArray<Item>(input: Item | ReadonlyArray<Item> | Array<Item>) {
    if (Array.isArray(input)) {
      return input;
    }
    return [input];
  }

  const [first] = makeArray([{ a: "1" }, { a: "2" }, { a: "3" }] as const);

  // No error!
  first.a;
});

to pass but not both at the same time.

With

interface ArrayConstructor {
  isArray<T>(arg: true extends TSReset.IsAny<T> ? never : T): arg is true extends TSReset.IsAny<T> ? never : T extends unknown[] | readonly unknown[] ? T : T[] extends T ? T[] : never;
}

the first passes and with

interface ArrayConstructor {
  isArray<T>(arg: true extends TSReset.IsAny<T> ? never : T): arg is true extends TSReset.IsAny<T> ? never : T extends unknown[] | readonly unknown[] ? T : T[] extends T ? T[] : never;
  isArray<T>(arg: T[] extends T ? T | T[] : never): arg is T[] extends T ? T[] : never;
  isArray<T>(arg: T): arg is T extends unknown[] | readonly unknown[] ? T : T[] extends T ? T[] : never;
}

the second passes.

Any help to get both to pass would be greatly appreciated.

DeepDoge commented 1 year ago
doNotExecute(async () => {
  function makeArray<Item>(input: Item) {
    if (Array.isArray(input)) {
      return input;
    }
    return [input];
  }

  const [first] = makeArray([{ a: "1" }, { a: "2" }, { a: "3" }] as const);

  // No error!
  first.a;
});

In the test above, you get a value, and if that value is an array, you just return it, if its not an array you place it inside an array and return it.

So as far as typescript knows that makeArray function can either return [Item] or Item, so it's return type is Item | [Item]. It doesn't care about your runtime logic. So this is a typescript issue, how it works, can't fix it.

Normally to make it give the right type you would define the makeArray function like this.

function makeArray<Item>(input: Item): 
    Item extends Array<any> ? Item : 
    Item extends ReadonlyArray<any> ? Item : 
    [Item]
  {
     if (Array.isArray(input)) {
       return input as any;
     }
     return [input] as any;
  }

So, basically you have to repeat your runtime logic with types. It's a typescript issue, not an issue with isArray().

As far as I know, but a second opinion won't hurt, maybe I'm missing something. Tbh, I was thinking about making my own scripting or programming language to overcome this issue and more.

Mickemouse0 commented 1 year ago

Thanks @DeepDoge for your answer. If @mattpocock agrees I could just remove that test and it would be ready from my side.

Maxim-Mazurok commented 1 year ago

I've tried this and it didn't work for my case. However this PR seems to work for my case where I want to narrow type from string or string array:

const token: string | readonly string[];
if (Array.isArray(token)) throw new Error("Token is an array");
console.log(token); // token is `string`, not `string | readonly string[]` here (as expected)
Mickemouse0 commented 1 year ago

@mattpocock I removed the failing test based on this comment. This PR is now redo to be merged

Mickemouse0 commented 2 months ago

@mattpocock Are the changes in this PR something that will be considered?