TypeScript: Wrongly infers any[] from Array.prototype.flat, Array.prototype.concat

TypeScript Version: 3.2.4, 3.3.0-dev.20190126

Search Terms: array, flat, concat, any, unsound

Code

const a: boolean[] = [[17], ["foo"]].flat();  // wrongly accepted
const b: boolean[] = Array.prototype.concat([17], [19], [21]);  // wrongly accepted

Using tsc --strict -t esnext.

(What I’m really trying to do: find a type safe way to concatenate an array of arrays. It’d be nice if [].concat(...arrays) were accepted, which I think would be a consequence of #26976, but that’s not the issue I’m reporting here. arrays.flat() and Array.prototype.concat(...arrays) are accepted but apparently provide no type safety.)

Expected behavior: [[17], ["foo"]].flat() should be typed (number | string)[] (if it’s accepted at all), and Array.prototype.concat([17], [19], [21]) should be typed number[]. In both cases the assignment to boolean[] should be rejected.

Actual behavior: TypeScript infers any[] for both right hand sides (even with --noImplicitAny implied by --strict), and accepts the assignments to boolean[] with no complaints.

Playground Link: Playground doesn’t support esnext, but here’s the second line.

Related Issues: #19535 for concat (but that one may have more to do with the difficulty of typing concat’s weird behavior when non-arrays are passed?), #26976

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 28 (10 by maintainers)

Most upvoted comments

The array element type should be unwrappable pretty easily:

type Elem<X> = X extends (infer Y)[] ? Y : X;

interface Array<T> {
    flat(depth: 7): Elem<Elem<Elem<Elem<Elem<Elem<Elem<Elem<this>>>>>>>>[];
    flat(depth: 6): Elem<Elem<Elem<Elem<Elem<Elem<Elem<this>>>>>>>[];
    flat(depth: 5): Elem<Elem<Elem<Elem<Elem<Elem<this>>>>>>[];
    flat(depth: 4): Elem<Elem<Elem<Elem<Elem<this>>>>>[];
    flat(depth: 3): Elem<Elem<Elem<Elem<this>>>>[];
    flat(depth: 2): Elem<Elem<Elem<this>>>[];
    flat(depth?: 1): Elem<Elem<this>>[];
    flat(depth: 0): Elem<this>[];
    flat(depth?: number): any[];
}

If I do so then I get correct errors:

const x = [1, [[2]]];
const y: never = x.flat();

produces

Type '(1 | [2])[]' is not assignable to type 'never'.

One option that would improve type safety would be to change the any[] overload of flat() to unknown[] and likewise for the typing of Array.prototype. This would force you to use a type assertion and prevent accidents like in the OP, but I don’t know whether it would go against that “balance between correctness and productivity”; in my experience the design seems to deliberately avoid creating situations that force a cast and prefer to use any in these situations. BUT I admit the possibility that that might only be because unknown is relatively new. 😄

@RyanCavanaugh The goalposts didn’t move as a result of the discussion AFAIK, the issue is still as described in the OP.

Anyway, I’ve put in my 2c (there, um, may have been some a lot of inflation), so I’ll duck out at this point and we can wait to see what the TS team thinks about this.