Skip to content
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

Improvment to Array.isArray #151

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Mickemouse0
Copy link

This PR improves the type inference of Array.isArray.

It builds on the changes made in #56 and #23 to solve #48.

@Mickemouse0
Copy link
Author

@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
Copy link
Author

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
Copy link

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
Copy link
Author

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

@@ -1,3 +1,3 @@
interface ArrayConstructor {
isArray(arg: any): arg is unknown[];
isArray<T>(arg: 0 extends 1 & T ? never : T): arg is 0 extends 1 & T ? never : T extends unknown[] | readonly unknown[] ? T : T[] extends T ? T[] : never;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you saw my comment here -- you can simplify this by removing the check for unknown[] entirely.

Suggested change
isArray<T>(arg: 0 extends 1 & T ? never : T): arg is 0 extends 1 & T ? never : T extends unknown[] | readonly unknown[] ? T : T[] extends T ? T[] : never;
isArray<T>(arg: 0 extends 1 & T ? never : T): arg is 0 extends 1 & T ? never : T extends readonly unknown[] ? T : T[] extends T ? T[] : never;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahrjarrett Thanks, I saw your comment but it slipped my mind while I was sitting with the code

@Maxim-Mazurok
Copy link

Maxim-Mazurok commented Jun 23, 2023

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 Mickemouse0 marked this pull request as ready for review June 23, 2023 09:43
@Mickemouse0
Copy link
Author

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

Comment on lines 1 to 3
interface ArrayConstructor {
isArray(arg: any): arg is unknown[];
isArray<T>(arg: true extends TSReset.IsAny<T> ? never : T): arg is true extends TSReset.IsAny<T> ? never : T extends ReadonlyArray<unknown> ? T : Array<T> extends T ? Array<T> : never;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I agree with the decision to disallow any, since narrowing a value from any to unknown[] seems like a valid thing to want to do.

Setting that aside, here's another way you could accomplish the same thing:

Suggested change
interface ArrayConstructor {
isArray(arg: any): arg is unknown[];
isArray<T>(arg: true extends TSReset.IsAny<T> ? never : T): arg is true extends TSReset.IsAny<T> ? never : T extends ReadonlyArray<unknown> ? T : Array<T> extends T ? Array<T> : never;
}
interface ArrayConstructor {
isArray<T>(arg: NotAny<T>): arg is Extract<NotAny<T>, readonly unknown[]>;
}
type NotAny<T> = 0 extends 1 & T ? never : T

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants