Skip to content

Unable to call map on array of variable depth #35045

Description

@danvk

I'm trying to write a function which transforms the coordinates of any type of GeoJSON geometry. The coordinates of a Geometry can be either number[] (a Point), number[][] (a LineString), number[][][] (a Polygon) or number[][][][] (a MultiPolygon). I have a recursive implementation which works but does not type check.

TypeScript Version: 3.7.2

Search Terms:

  • 2349

Code

type Position = number[];

function isNestedArray<T extends any[]>(x: T | T[]): x is T[] {
  return Array.isArray(x[0]);
}

export function transformCoordArray(
  coords: Position | Position[] | Position[][],
  fn: (pt: Position) => Position
): any[] {
  if (isNestedArray(coords)) {
    coords;  // type is Position[] | Position[][]
    return coords.map(c => transformCoordArray(c, fn));
    //            ~~~ This expression is not callable.
  } else {
    coords;  // type is Position
    return fn(coords);
  }
}

(playground)

The full error message is:

This expression is not callable.
  Each member of the union type '
   (<U>(callbackfn: (value: Position, index: number, array: Position[]) => U, thisArg?: any) => U[])
 | (<U>(callbackfn: (value: Position[], index: number, array: Position[][]) => U, thisArg?: any) => U[])
' has signatures, but none of those signatures are compatible with each other.(2349)

The call is valid because c's type should be Position | Position[], which is assignable to Position | Position[] | Position[][].

Interestingly, I can make the error go away by assigning to a new variable which has a broader type ((Position | Position[])[]):
(playground)

export function transformCoordArray(
  coords: Position | Position[] | Position[][],
  fn: (pt: Position) => Position
): any[] {
  if (isNestedArray(coords)) {
    coords;  // type is Position[] | Position[][]
    const c2: (Position | Position[])[] = coords;
    return c2.map(c => transformCoordArray(c, fn));  // ok
  } else {
    coords;  // type is Position
    return fn(coords);
  }
}

Expected behavior:

No error.

Actual behavior:

The given error.

Playground Link: playground

Related Issues:

Activity

  1. jcalz commented on Nov 12, 2019

    @jcalz
    Contributor

    Duplicate of #7294 (see comment). Related to #29011.

    Mentioned in documentation as caveats for improved behavior for calling union types:

    This new behavior only kicks in when at most one type in the union has multiple overloads, and at most one type in the union has a generic signature. That means methods on number[] | string[] like map (which is generic) still won’t be callable.

  2. thorn0 commented on Nov 12, 2019

    @thorn0

    While methods like push are obviously problematic for number[] | string[], it looks safe to allow methods like map or every for readonly number[] | readonly string[]. Has this (special treatment of read-only arrays when merging union signatures) already been discussed in some issue?

  3. thorn0 commented on Nov 15, 2019

    @thorn0

    We are trying to gradually introduce type checking in the Prettier project and facing this issue.

    Prettier works with ASTs that come from different parsers.

    E.g. suppose we have an object node of type import('estree').Node | import('@typescript-eslint/typescript-estree').TSESTree.Node and code like this:

    if (node.type === "ObjectExpression") {
        return node.properties.every(
          p => ...
        );
      }

    Even though node.properties are correctly inferred to be of type ObjectLiteralElementLike[] | Property[] (because both estree and @typescript-eslint/typescript-estree define ObjectExpression, not exactly compatible though), p in the every callback is of type any. 😢

  4. hinell commented on Nov 16, 2019

    @hinell

    I think it's inherently bad idea to rely on compiler to figure out the output of the transformCoordArray() meanwhile coords: ... argument varies.
    I think much better way either to create overloads (checkout playground):

      ...
      export function transformCoordArray(coords: Position[][], fn: PosTransfCb): Position[][]
      export function transformCoordArray(coords: Position[], fn: PosTransfCb): Position[]
      export function transformCoordArray(coords: Position, fn: PosTransfCb): Position
      ...

    or assert coords:

    (<any> coords).map(c => transformCoordArray(c, fn)) as Position | Position[] | Position[][];
  5. jcalz commented on May 27, 2021

    @jcalz
    Contributor

    Fixed by #42620 I think?

  6. danvk commented on May 27, 2021

    @danvk
    ContributorAuthor

    Yep! The error in the example in the original issue goes away with 4.3: playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions