Improve typings of Array.map when called on tuples
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.
Duplicate of #12548, was addressed in #11252.
Unfortunately I believe that
- it potentially broke subtyping arrays (this might not be true)
- it didn't work for arbitrary arities
- it and slowed down the compiler a lot
so we reverted it in #16223.
@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 }));
Yes, I opened this issue in light of TypeScript 3.1 mappable tuple and array types. So maybe things have changed?
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, I believe the new overload you wrote above does break for Array subtypes. RegExp matching makes for a nice example
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.
After updating 45kloc to noUncheckedIndexedAccess = true, indeed having this feature would increase type safety significantly in our case.
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?
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.
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[]
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]
seeing this issue is still open, is there any chance we can still get proper tuple typing in built in functions like .map()?
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?