Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typed arrays and union types

I work with typed arrays a lot and a lot of my functions really should be able to work with any sort of typed array (e.g., summing a Uint8Array or a Float32Array). Sometimes, I can get away with just a simple type union, but often I keep running into the same error.

A simple example:

type T1 = Uint8Array;
type T2 = Int8Array;
type T3 = Uint8Array | Int8Array;

// No problems here:
const f1 = (arr: T1) => arr.reduce((sum, value) => sum + value);
const f2 = (arr: T2) => arr.reduce((sum, value) => sum + value);

// Does not work:
const f3 = (arr: T3) => arr.reduce((sum, value) => sum + value);

The error on f3 is:

Cannot invoke an expression whose type lacks a call signature. Type '
{
    (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number): number;
    (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Uint8Array) => number, initialValue: number): number;
    <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Uint8Array) => U, initialValue: U): U;
} | {
    (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int8Array) => number): number;
    (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Int8Array) => number, initialValue: number): number;
    <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Int8Array) => U, initialValue: U): U; 
}' has no compatible call signatures.ts(2349)

According to the docs:

If we have a value that has a union type, we can only access members that are common to all types in the union.

The way I'm using reduce here is common to all arrays, but I surmise the problem is the optional 4th argument (which for Uint8Array.prototype.reduce is a Uint8Array but for Int8Array.prototype.reduce is an Int8Array).

Is there a simple work-around for this? Or do I need to write a generics implementation for each of map, reduce, filter?

like image 778
Andrew Avatar asked Jul 04 '19 08:07

Andrew


2 Answers

There has always been an issue in invoking unions of functions. Until recently the rule was that no invocation would be allowed at all, but since 3.3 (PR) invocation is allowed with some caveats. The big one you are hitting here is that if the constituents of the union both have generic signatures the call will still not be allowed. So for example on simple arrays one forEach can be called (no generic type parameters on it), while reduce can't be called (since both reduce from string[] and from number[] have a generic type parameter):

declare let o: string[] | number[];
o.forEach((e: number | string) => console.log(e)); // ok 
o.reduce((e: number | string, r: number | string) => e + ' ' + r) //err

This does mean having a union of array types is difficult to use and only allows invocation of a very small set of methods (most Array methods have generic type parameters).

This also applies to Uint8Array and Int8Array which while don't inherit array have most of the same methods.

There is no good solution here, the simplest work around is to assert the variable to one of the types and go with that (assuming you don't use the array callback parameter it should be ok)

const f3 = (arr: T3) => (arr as Uint8Array).reduce((sum, value, array /* no good inferred to Uint8Array */) => sum + value);

Or fall back to one of the functions you can invoke

const f4 = (arr: T3) => {
    let sum = 0;
    (arr as Uint8Array).forEach((val)=> sum + val)
} 
like image 66
Titian Cernicova-Dragomir Avatar answered Sep 29 '22 01:09

Titian Cernicova-Dragomir


There is simple workaround. Declare interface with common method signature

type T3 = Uint8Array | Int8Array;

interface HasReduce {
    reduce(c: (p: number, n: number) => number): number; // common callback signture with 2 arguments
}

function someLogic(arr: HasReduce): number { 
    return arr.reduce((sum, value) => sum + value);
}

declare var v : T3;
someLogic(v); // OK

So you can declare HasMap, HasFilter and combine them.

like image 40
Nail Achmedzhanov Avatar answered Sep 29 '22 01:09

Nail Achmedzhanov