Skip to content

Array type refinement on .every #43989

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

Closed
5 tasks done
cakoose opened this issue May 7, 2021 · 3 comments
Closed
5 tasks done

Array type refinement on .every #43989

cakoose opened this issue May 7, 2021 · 3 comments

Comments

@cakoose
Copy link

cakoose commented May 7, 2021

Suggestion

πŸ” Search Terms

array refine every

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

A way to make Array.every refine the type of the argument array. This isn't sound, but I think the unsoundness is similar in spirit TypeScript's usual unsoundness around mutability.

πŸ“ƒ Motivating Example

Playground link

It would be nice if this worked:

function max1<T>(stuff: Array<string | number>): number {
    if (stuff.every(x => typeof x === 'number')) {
        Math.max(...stuff); // Error: 'string | number' is not assignable to 'number'.

    }
    return -1;
}

I tested it out with my own every function. But to get my original example to work, I needed to annotate the return type of my predicate:

declare function every<T, R extends T>(array: Array<T>, predicate: (value: T) => value is R): array is Array<R>;

function max2<T>(stuff: Array<string | number>): number {
    if (every(stuff, x => typeof x === 'number')) { // Error: (x: string | number) => boolean' is not assignable to '(value: string | number) => value is string | number'
        Math.max(...stuff);
    }
    return -1;
}

// Works with explicit type annotation on predicate return type.
function max3<T>(stuff: Array<string | number>): number {
    if (every(stuff, (x): x is number => typeof x === 'number')) {
        Math.max(...stuff);
    }
    return -1;
}

πŸ’» Use Cases

The original inspiration for this line of thought came from writing a zip-style function:

function* zip<T>(...iterables: Array<Iterable<T>>): Iterable<Array<T>> {
    if (iterables.length === 0) throw new Error('iterables is empty');
    // Explicit annotation on 'iterators' because Iterator's second type argument defaults to 'any', which masks the 'r.value' error below.
    const iterators: Array<Iterator<T, unknown, never>> = iterables.map(it => it[Symbol.iterator]());

    while (true) {
        const results = iterators.map(it => it.next());
        if (!every(results, r => r.done !== true)) { // Error here, but I don't know what annotation to write.
            return;
        }
        yield results.map(r => r.value); // Error here, because r.value might be for a 'done' iterator
    }
}
@MartinJohns
Copy link
Contributor

MartinJohns commented May 7, 2021

Duplicate of #38390.

In order for every() to narrow the type you must pass in a type guard function. You currently don't do that, but if you would it would work as you expect.

This is not a type guard: x => typeof x === 'number'

This is a type guard: (x): x is number => typeof x === 'number'

@cakoose
Copy link
Author

cakoose commented May 7, 2021

Thanks!

@MartinJohns's original comment had some additional information that was helpful to me:

In order for every() to narrow the type you must pass in a type guard function. You currently don't do that, but if you would it would work as you expect.

This is not a type guard: x => typeof x === 'number'

This is a type guard: (x): x is number => typeof x === 'number'

@cakoose cakoose closed this as completed May 7, 2021
@MartinJohns
Copy link
Contributor

I removed that part of the comment because you actually mentioned those parts in your issue already. :-) Or so I thought, re-reading your issue I think I too hastely removed that comment.

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

No branches or pull requests

2 participants