Skip to content

Array type refinement on .every #43989

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # 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.

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

No branches or pull requests

2 participants