Skip to content
Advertisement

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).

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!

Playground link to code

Advertisement