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).
Advertisement
Answer
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!