TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Improve typings of Array.map when called on tuples

Open zroug opened this issue 6 years ago • 14 comments

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:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code (At least I think so...)
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

zroug avatar Feb 09 '19 20:02 zroug

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.

DanielRosenwasser avatar Feb 12 '19 19:02 DanielRosenwasser

@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 }));

dragomirtitian avatar Feb 12 '19 20:02 dragomirtitian

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

zroug avatar Feb 12 '19 23:02 zroug

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.

dragomirtitian avatar Feb 12 '19 23:02 dragomirtitian

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

jdmoody avatar Apr 26 '20 08:04 jdmoody

If it helps, here's another use-case:

const stringCompare = (to: string, from: string) =>
  to < from ? -1 : to > from ? 1 : 0;

const reverseString = (value: string) => value.split('').reverse().join('');

const stringCompareFromEnd = (
  ...values: [to: string, from: string]
) => stringCompare(...values.map(reverseString));

In the last line there is an error: stringCompare expects 2 arguments, but gets 0+.

There's also many more use-cases when --noUncheckedIndexedAccess is enabled.

ivan7237d avatar Nov 09 '20 22:11 ivan7237d

After updating 45kloc to noUncheckedIndexedAccess = true, indeed having this feature would increase type safety significantly in our case.

stephanemagnenat avatar Feb 11 '21 08:02 stephanemagnenat

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?

Michael-Ziluck avatar Feb 01 '22 23:02 Michael-Ziluck

I thought so, too, but couldn't figure out a way. Even with instantiation expressions around the corner (https://github.com/microsoft/TypeScript/pull/47607#issuecomment-1026475134), 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.

buschtoens avatar Feb 02 '22 00:02 buschtoens

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 avatar Oct 12 '23 18:10 csvn

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]

csvn avatar Oct 12 '23 18:10 csvn

seeing this issue is still open, is there any chance we can still get proper tuple typing in built in functions like .map()?

marcospgp avatar Jun 17 '25 13:06 marcospgp

Anytime I have to use .map() on a tuple type, I have to manually cast the output to correct the types. It would be great if TS handles this out of the box. Maybe with performance boost TSGo gives, this can be implemented without big consequences? Or with a tsconfig setting that is false by default?

Anoesj avatar Sep 24 '25 07:09 Anoesj