'TypeScript function that works on all numerical array types
I am trying to write a function that works on all of the JavaScript array types, e.g. on number[]
, Float32Array
etc. It should return the same type that it gets as a parameter. A simple example would be:
function addOne<T>(a: T) : T {
return a.map((x: number) => x + 1);
}
The function should be able to use all methods common to all array types (not just map
).
I also tried
type NumberArray<T> = T extends Array<number>
? Array<number>
: Float32Array; // other array types skipped for brevity
function addOne<T>(a: NumberArray<T>): NumberArray<T> {
return a.map((x: number) => x + 1);
}
but I get
TS2322: Type 'number[] | Float32Array' is not assignable to type 'NumberArray<T>'. Type 'number[]' is not assignable to type 'NumberArray<T>'.
What would the TypeScript signature of such a function be? I also want to be able to create several such function and pass them as a parameter to another function (all properly typed, of course). A trivial example would be:
function doSomethingWithArray(a, func) {
return func(a);
}
The type of a
should define which signature of func
is used.
I have no problems running this in JS, but when trying to add proper TS typing, the TS compiler complains (I am running with "strict": true
compiler option).
Solution 1:[1]
TypeScript does not have a built-in NumericArray
type of which Array<number>
and Float32Array
-et-cetera are subtypes that gives you access to all common methods. Nor can I think of a one-or-two line solution that will give that to you. Instead, if you really need this, I'd suggest you create your own type. For example:
interface NumericArray {
every(predicate: (value: number, index: number, array: this) => unknown, thisArg?: any): boolean;
fill(value: number, start?: number, end?: number): this;
filter(predicate: (value: number, index: number, array: this) => any, thisArg?: any): this;
find(predicate: (value: number, index: number, obj: this) => boolean, thisArg?: any): number | undefined;
findIndex(predicate: (value: number, index: number, obj: this) => boolean, thisArg?: any): number;
forEach(callbackfn: (value: number, index: number, array: this) => void, thisArg?: any): void;
indexOf(searchElement: number, fromIndex?: number): number;
join(separator?: string): string;
lastIndexOf(searchElement: number, fromIndex?: number): number;
readonly length: number;
map(callbackfn: (value: number, index: number, array: this) => number, thisArg?: any): this;
reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number): number;
reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number, initialValue: number): number;
reduce<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: this) => U, initialValue: U): U;
reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number): number;
reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number, initialValue: number): number;
reduceRight<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: this) => U, initialValue: U): U;
reverse(): this;
slice(start?: number, end?: number): this;
some(predicate: (value: number, index: number, array: this) => unknown, thisArg?: any): boolean;
sort(compareFn?: (a: number, b: number) => number): this;
toLocaleString(): string;
toString(): string;
[index: number]: number;
}
That's kind of long, but I created it by merging the existing array typings in lib.es5.d.ts
, and changing any reference to the particular array type with the polymorphic this
type, meaning "the current subtype of the NumericArray
interface". So, for example, Array<number>
's map()
method returns an Array<number>
, while a Float32Array
's map()
method returns a Float32Array
. The this
type can be used to represent this relationship between the array type and the return type.
If you care about post-ES5 functionality you can go and merge those methods in also, but this should be enough to demonstrate the basic approach.
You could try to write something that computes NumericArray
programmatically, but I wouldn't want to. It is likely to be more fragile and more confusing than the manual NumericArray
definition above, and probably take nearly as many lines.
Then, I'd write your addOne()
in terms of NumericArray
:
function addOne<T extends NumericArray>(a: T): T {
return a.map((x: number) => x + 1);
}
And you can verify that it works as expected for Array<number>
and Float32Array
:
const arr = addOne([1, 2, 3]);
// const arr: number[]
console.log(arr); // [2, 3, 4];
arr.unshift(1); // okay
const float32Arr = addOne(new Float32Array([1, 2, 3]));
// const float32Arr: this
console.log(float32Arr) // this: {0: 2, 1: 3, 2: 4}
console.log(float32Arr.buffer.byteLength); // 12
And your doSomethingWithArray
would look like this:
function doSomethingWithArray<T extends NumericArray, R>(a: T, func: (a: T) => R) {
return func(a);
}
console.log(doSomethingWithArray([4, 5, 6], x => x.unshift(3))); // 4
console.log(doSomethingWithArray(new Int8Array([1, 2, 3, 4, 5]), x => x.byteLength)); // 5
Looks good!
Solution 2:[2]
I was reminded of this question today and came up with something I find interesting:
function addOne <T extends NumericArray[number]> (a: T)
: Narrow<T, NumericArray> {
return a.map(x => x + 1) }
type NumericArray = [
Array<number>,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array
];
type Narrow<
T, L extends unknown[] = [],
R1 extends unknown[] = {
[K in keyof L]: T extends L[K] ? { type: T } : never
},
R2 = unknown extends T ? never : R1[number],
> = [R2] extends [{ type: unknown }] ? R2['type'] : never
I like how it has a different maintainability profile than jcalz's answer, which I still believe made more sense for your use case because the API is cleaner and you had to write a lot of functions for the same set of variants. That was a lot of grunt work though and I would see myself reaching for this second solution if I had to do this for different sets of variants.
I guess the API could be a little cleaner if Narrow
took a union instead of a tuple but I don't know how computationally intensive that would be to add this step.
Solution 3:[3]
Your last question has a simple solution
function apply<T, U>(a: T, func: (x: T) => U): U {
return func(a);
}
UPDATE
This seems to work and I fail to see what is unsafe about it.
type NumberArray =
{ map:(f:(x:number) => number) => any }
& ArrayLike<number>
& Iterable<number>;
function addOne<T extends NumberArray>(a: T):T {
return a.map((x: number) => x + 1)
}
I don't think it's possible to do what you want with number[]
and Float32Array
because there is no such thing as a Float32 type in Javascript and we can't express something like "I want a generic Wrapper
around a type Number | Float32 | ...
" in Typescript anyway. Without these 2 features I don't see how it could be done.
We could do something like that which makes Typescript happy:
type NumberArray =
{
map: Function
// define other methods there
}
& ArrayLike<number>
& Iterable<number>;
function addOne<T extends NumberArray>(a: T): T {
return a.map((x: number) => x + 1)
}
But it allows you to map
to a type other than number:
function addOne<T extends NumberArray>(a: T): T {
return a.map((x: number):string => x + '!') // woops
}
We could do the following:
type NumberArray =
{
map:(f:(x:number) => number) => NumberArray
// define other methods there
}
& ArrayLike<number>
& Iterable<number>;
function addOne<T extends NumberArray>(a: T): T {
return a.map((x: number) => x + 1)
}
but then we would get an error.
Type 'NumberArray' is not assignable to type 'T'.
'NumberArray' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ map: (f: (x: number) => number) => NumberArray; } & ArrayLike<number> & Iterable<number>
it can only be used like so, which looses type information (maybe it's OK for you though)
function addOne(a: NumberArray) {
return a.map((x: number):number => x + 1)
}
On a side note, I don't see the value of creating an addOne
function which can only work with array-like objects. That's very specific. The whole point of map
is to decouple the data structure from the transformation.
The following is more flexible because addOne
doesn't need to know about where it's going to be used and the type information is preserved:
const addOne = (x: number) => x + 1;
addOne(3);
[1, 2, 3].map(addOne);
Float32Array.from([1,2,3]).map(addOne);
It doesn't solve your problem though: as soon as we define a function which needs to accept a NumberArray
as argument we are back where we started...
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | geoffrey |
Solution 3 |