microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.32k stars 12.39k forks source link

Allow type narrowing for arrays using Array#forEach and assertion function #52389

Open tduyduc opened 1 year ago

tduyduc commented 1 year ago

lib Update Request

Configuration Check

My compilation target is ES5 and my lib is the default.

Missing / Incorrect Definition

Array type narrowing using Array#forEach with assertion function, in a fashion similar to Array#every, has not been defined. Also applies to ReadonlyArray.

Sample Code

const foo: (number | string)[] = ['aaa'];

function assertString(x: unknown): asserts x is string {
  if (typeof x !== 'string') throw new Error('Must be a string!');
}

foo.forEach(assertString);
foo[0].slice(0); // foo should be string[] from here

Documentation Link

Based on Array#every<S extends T> from lib.es5.d.ts.


A pull request for implementing this definition has been created #52388.

Tobias-Scholz commented 1 year ago

I like the idea but it does not work with the current overload resolution. The is no difference if callbackfn is an assertion function; the first overload is always chosen in this situation.

Here is what happens if the new overload is the first one:

interface Array<T> {
  forEach<S extends T>(callbackfn: (value: T, index: number, array: T[]) => asserts value is S, thisArg?: any): asserts this is S[];
  forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
}

[0, 1, 2].forEach(e => console.log(e))
// ~~~~~~~~~~~~~~ Error: Assertions require the call target to be an identifier or qualified name.

and when it is the second one:

interface Array<T> {
  forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
  forEach<S extends T>(callbackfn: (value: T, index: number, array: T[]) => asserts value is S, thisArg?: any): asserts this is S[];
}

const foo: (number | string)[] = ['aaa'];

function assertString(x: unknown): asserts x is string {
  if (typeof x !== 'string') throw new Error('Must be a string!');
}

foo.forEach(assertString);
foo[0].slice(0); 
//     ~~~~~ Error: Property 'slice' does not exist on type 'number'
tduyduc commented 1 year ago

I see... Seems like some changes have to be made on TypeScript compiler itself to let assertion function and neither-assertion-nor-type-guard-function overloads co-exist.