Typescript: Reduce without an initialValue should be illegal on a potentially empty array

Created on 25 Oct 2019  ·  6Comments  ·  Source: microsoft/TypeScript


TypeScript Version: 3.8.0-dev.20191025


Search Terms: reduce no initialvalue

Code

[].reduce((a,b) => a+b /*, initialvalue NOT specified*/);

Expected behavior:
Should not compile

Actual behavior:
Compiles and crashes:

~ $ tsc reduce.ts; echo $?
0
~ $ js reduce.js
reduce.js:1: TypeError: reduce of empty array with no initial value

Related Issues:
None relevant, https://github.com/microsoft/TypeScript/issues/28901 is trying to make TS accept something that it doesn't accept currently; I'm asking the opposite: that something which is accepted became not accepted.

Some ideas:
Retiring reduce with no initialValue from the type Array as this is unsafe to keep around; people should provide an initialValue if their array can be empty:

--- a/typescript/lib/lib.es5.d.ts   2019-10-25 12:33:06.312693040 +0200
+++ b/typescript/lib/lib.es5.d.ts   2019-10-25 12:34:11.071987865 +0200
@@ -1329,6 +1329,7 @@
       * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.
       * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
       */
-    reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T;
     reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;
     /**
       * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.

In order to use reduce without initialValue on arrays for which it will work, it's possible to introduce something that might or might not be along the lines of:

type NonEmptyArray<T> = [T, ...T[]];

This non-empty array type can safely reintroduce the reduce signature without initialValue, the very one I would like to retire from the (possibly empty) Array type.

People who want to live dangerously could still use ([] as NonEmptyArray).reduce(someOperation) if they absolutely refuse to provide an initialValue.

Suggestion Too Complex

Most helpful comment

In fact, there are no moments where there is no identity element, I wager. It would rather be the developer exploiting the mathematically clunky version of reduce (the one where you give no identity element that is).

To demonstrate, the above example,

arr.reduce((result, item) => result / item, /*no identity element*/);

…translates to English as "the first element of the list, divided by all others in turn", a sentence where I notice the part "the first element of the list", and which therefore leaves a gaping hole in the definition for the case of empty lists: it is not defined on them.

Emphasis on the fact that the first element is treated very differently from the rest, which should somehow be visible in the implementation. I believe the example is therefore more mathematically put as:

arr.slice(1).reduce((result, item) => result / item, arr[0]);

And the reference to the first element makes it clear as day that there will be a problem if we have nothing in the list.
…Although as it turns out, even tsc --strict doesn't complain about it either and we get undefined. Well, technically, indeed. But, augh!

I believe that TS should only allow static references, e.g. arr[2] to array indexes that provably exist.
With the method mentioned earlier:

let foo: [T, T, T ...T[]] = [9, 12, 4, 5, 14, 7];
foo[0], foo[1]; // ok
foo[2]; // ok
foo[3]; // error

I explore this because the topic of arrays with some guarantees on their length isn't irrelevant to in-depth handling of the present issue (which I'd very much like to explore solutions for).

All 6 comments

How about we just delete the .reduce() signature without an initialValue entirely? =P

It's rare that one uses the .reduce() method and performs an operation that does not have an identity element.

For example,

//Zero is the identity element of addition
arr.reduce((result, item) => result + item, 0);

//One is the identity element of multiplication
arr.reduce((result, item) => result * item, 1);

//Empty object is the identity element of adding properties to stuff
arr.reduce((result, item) => { result[item] = 0; return result; }, {} as Record<string, unknown>);

I'm just kidding about removing the signature entirely, by the way.

Of course, there are moments where there is no identity element,

arr.reduce((result, item) => result / item, /*no identity element*/);

In fact, there are no moments where there is no identity element, I wager. It would rather be the developer exploiting the mathematically clunky version of reduce (the one where you give no identity element that is).

To demonstrate, the above example,

arr.reduce((result, item) => result / item, /*no identity element*/);

…translates to English as "the first element of the list, divided by all others in turn", a sentence where I notice the part "the first element of the list", and which therefore leaves a gaping hole in the definition for the case of empty lists: it is not defined on them.

Emphasis on the fact that the first element is treated very differently from the rest, which should somehow be visible in the implementation. I believe the example is therefore more mathematically put as:

arr.slice(1).reduce((result, item) => result / item, arr[0]);

And the reference to the first element makes it clear as day that there will be a problem if we have nothing in the list.
…Although as it turns out, even tsc --strict doesn't complain about it either and we get undefined. Well, technically, indeed. But, augh!

I believe that TS should only allow static references, e.g. arr[2] to array indexes that provably exist.
With the method mentioned earlier:

let foo: [T, T, T ...T[]] = [9, 12, 4, 5, 14, 7];
foo[0], foo[1]; // ok
foo[2]; // ok
foo[3]; // error

I explore this because the topic of arrays with some guarantees on their length isn't irrelevant to in-depth handling of the present issue (which I'd very much like to explore solutions for).

Would be cool to have type guards like,

declare const arr : T[];
if (arr.length > 0) {
  //arr is now `[T, ...T[]]`
}
if (arr.length >= 2) {
  //arr is now `[T, T, ...T[]]`
}
if (arr.length == 4) {
  //arr is now `[T, T, T, T]`
}

This way, the overload for .reduce(this : [T, ...T[]], reducer) would be way easier to use.


I think there's a proposal for such a type guard... I'm not sure.

[EDIT]

Found it

https://github.com/microsoft/TypeScript/issues/28837

This level of enforcement, while coherent, would be far far above anything currently on the table, to say nothing of consistency -- even today arr[0] is allowed regardless of type information!

Many times arrays are known to be non-empty by construction and this would introduce a lot of unneeded friction.

There are two sides to this indeed:

  • on the one hand, this does go farther than the current set of checks;
  • on the other hand, to typecheck basic usage of JS built-ins correctly, one has to go this far.

I suggest this rule of thumb: when the JS engine throws a TypeError at run time, something is wrong with the static typing.

--

Concerning the "unneeded" friction (thinking of existing code, I assume), I think that is an argument for the part of the debate about whether to have array length checks as a default.

I am aware that people who care less and do not actually rely on the type system, considering it a bonus, will feel it's a bother if they need to satisfy the type checker or forcefully type-assert that they know what they are doing.

However, people who use --strict typically seek and expect pedantic behaviour from the compiler, and feel betrayed when type checking overlooks simple and detectable, yet deadly mistakes:

[…] even made me lose trust in the type system […] https://github.com/Microsoft/TypeScript/issues/28682

People who decide to use a type system do it specifically because they find friction far more desirable than potential run time errors.

[edit] What I am questioning is whether this could indeed be "Working as Intended", not the fact that this can be classified as an improvement request rather than a bug report.

I've changed the labels - let me know if there are others you think would be better

Was this page helpful?
0 / 5 - 0 ratings