Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Improve typings of Array.map when called on tuples #29841

Open
5 tasks done
zroug opened this issue Feb 9, 2019 · 11 comments · May be fixed by #50046
Open
5 tasks done

Improve typings of Array.map when called on tuples #29841

zroug opened this issue Feb 9, 2019 · 11 comments · May be fixed by #50046
Assignees
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@zroug
Copy link

zroug commented Feb 9, 2019

Search Terms

  • Array.map
  • map tuple

Suggestion

Using Array.map on a tuple should return a tuple instead of an array. In one of my projects I could achieve that using

declare interface Array<T> {
    map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): { [K in keyof this]: U };
}

I haven't encountered any negative side effects.

Use Cases

This can be useful when you want to use tuples as a fixed length array.

Examples

type Vec3D = [number, number, number];
let vec: Vec3D = [1, 2, 3];
let scaledVec: Vec3D = vec.map(x => 2 * x);

This is currently an error:
https://www.typescriptlang.org/play/#src=type%20Vec3D%20%3D%20%5Bnumber%2C%20number%2C%20number%5D%3B%0D%0Alet%20vec%3A%20Vec3D%20%3D%20%5B1%2C%202%2C%203%5D%3B%0D%0Alet%20scaledVec%3A%20Vec3D%20%3D%20vec.map(x%20%3D%3E%202%20*%20x)%3B

But with the proposed change it would not be an error:
https://www.typescriptlang.org/play/#src=declare%20interface%20Array%3CT%3E%20%7B%0D%0A%20%20%20%20map%3CU%3E(callbackfn%3A%20(value%3A%20T%2C%20index%3A%20number%2C%20array%3A%20T%5B%5D)%20%3D%3E%20U%2C%20thisArg%3F%3A%20any)%3A%20%7B%20%5BK%20in%20keyof%20this%5D%3A%20U%20%7D%3B%0D%0A%7D%0D%0A%0D%0Atype%20Vec3D%20%3D%20%5Bnumber%2C%20number%2C%20number%5D%3B%0D%0Alet%20vec%3A%20Vec3D%20%3D%20%5B1%2C%202%2C%203%5D%3B%0D%0Alet%20scaledVec%3A%20Vec3D%20%3D%20vec.map(x%20%3D%3E%202%20*%20x)%3B

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code (At least I think so...)
  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Feb 12, 2019

Duplicate of #12548, was addressed in #11252.

Unfortunately I believe that

  1. it potentially broke subtyping arrays (this might not be true)
  2. it didn't work for arbitrary arities
  3. it and slowed down the compiler a lot

so we reverted it in #16223.

@dragomirtitian
Copy link
Contributor

@DanielRosenwasser The arity problem can be solved now that we can map tuples. Not sure about the performance, but not having a ton of overloads might improve it a bit

interface Array<T> {
  map<TThis extends Array<T>, U>(this: TThis, fn: (v: T) => U): { [K in keyof TThis]: U }
}

function tuple<T extends any[]>(...a: T) {
  return a;
}

let x = tuple(1, 2).map(v => v.toString()); //[string, string]
let o = tuple(1, 2, 3).map(n => ({ n }));

@zroug
Copy link
Author

zroug commented Feb 12, 2019

Yes, I opened this issue in light of TypeScript 3.1 mappable tuple and array types. So maybe things have changed?

@dragomirtitian
Copy link
Contributor

Also from what I see #16223 reverted a lot more than just changes to map, it rolled back the preservation of thisArg in the callback on multiple array functions. Inferring this on any array function might incur a significant perf penalty, for something that (at least in my experience) is not all that used (ie thisArg).

This issue requests improve just map this would seem like a wider use case and less of a perf penalty since we are just talking about map and not other array functions.

@jdmoody
Copy link

jdmoody commented Apr 26, 2020

@dragomirtitian, I believe the new overload you wrote above does break for Array subtypes. RegExp matching makes for a nice example

@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Aug 31, 2020
@RyanCavanaugh RyanCavanaugh removed this from the TypeScript 4.0.1 milestone Aug 31, 2020
@Michael-Ziluck
Copy link

Now that variadic tuple types are added, this should be a whole lot easier to support. Any chance on seeing this in a release any time soon?

@buschtoens
Copy link

I thought so, too, but couldn't figure out a way. Even with instantiation expressions around the corner (#47607 (comment)), it still seems impossible. AFAICT the core problem is that generics (e.g. a generic callback function for .map(cb)) cannot cross scope boundaries. You would need higher-kinded types for that.

A higher-kinded type is to the type system what a higher-order function is to functions. For instance, .map(cb: (value, index, array) => mappedValue) is a higher-order function. It's a function that does not (only) directly operate on concrete values (like Math.max(...n: number[])), but also on functions themselves (cb), invoking them dynamically with variable arguments.

So functions can cross these boundaries, but generic types cannot. Therefore we lose the ability to dynamically infer the function return type in dependence of the variable function arguments.

@csvn
Copy link

csvn commented Oct 12, 2023

I was trying to preserve the tuple sizes for as well using const type parameters (Typescript playground). Unfortunately, as soon as Array.map() is used, I lose the type information I have. It would be awesome to be able to preserve tuple size. Having to explicitly write out a tuple is often longer and annoying. For the cases you don't want a tuple using T[] to make the type more generic is much easier.

function lengths<const T extends readonly string[]>(...names: T) {
  return names.map(n => n.length);
}

const result = lengths('foo', 'baaaar', 'baaaaaaaaaaaz');
//  number[]

@csvn
Copy link

csvn commented Oct 12, 2023

And of course, I just found a workaround for my issue above right after I posted. In my case above I can use as { [K in keyof T]: number; } to cast the return value before returning it (Playground link.

function lengths<const T extends readonly string[]>(...names: T) {
  return names.map(n => n.length) as { [K in keyof T]: number; };
}

const result = lengths('foo', 'baaaar', 'baaaaaaaaaaaz');
//  readonly [number, number, number]

jangxyz added a commit to jangxyz/html-tagged-templates that referenced this issue May 20, 2024
no function overloads.
no passing types as generics.

instead, we rely on two things:

* [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html),
  which turns out to be useful on [converting
  tuples](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#mapped-types-on-tuples-and-arrays).
  (as an example, see:
    - https://stackoverflow.com/questions/68180531/how-to-use-variadic-tuple-types-in-typescript
    - https://stackoverflow.com/questions/72091158/typescript-is-it-possible-to-define-a-variadic-function-that-accepts-different
    - microsoft/TypeScript#29841
  )

  This means that by doing:

  ```typescript
  function htmlMultipleFn<T extends string[]>(htmlStrings: [...T]): { [I in keyof T]: DeterminedNode<T[I]> };
  ```

  This receives a 'tuple' of strings, and returns *an object that takes
  key of the tuple, and a value of some different computation* -- which
  is effectively a map of an array.

  What's better, is that it also 'remembers' the exact length, because
  it is considered as a tuple, not an array -- that's why we have `[...T]` for
  arg type instead of `T`.

* [Const Type Parameter](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters)

  Now, with only the one above, the following works:

  ```typescript
  const result = htmlMultipleFn(['<div>']) // [HTMLDivElement]
  ```

  but this doesn't:

  ```typescript
  const [notDivEl] = htmlMultipleFn(['<div>']) // Node
  ```

  To make T stay as a tuple instead of an array, we apply const type parameter:

  ```typescript
  function htmlMultipleFn<const T extends string[]>(htmlStrings: [...T]): { [I in keyof T]: DeterminedNode<T[I]> };
  ```

NOTE we had to remove tests which received node type generics, because
we are not using those types anymore:

```typescript
// 🚫 We can't do this any more
//const [divEl, pEl] = htmlMultipleFn<[HTMLDivElement, HTMLParagraphElement]>([
//  "<div>Hi there,</div>",
//  "<p>I am here</p>",
//])
```

To force the type, we need to do type assertion:

```typescript
// ✅ Do this instead
const [divEl, pEl] = htmlMultipleFn(
  ["<div>Hi there,</div>", "<p>I am here</p>"]
) as [HTMLDivElement, HTMLParagraphElement]
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet