Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: Generics type definition for TypedArray

I am trying to write a function that expands / shrinks TypedArray by taking an arbitrary TypedArray as an input and returns a new same-typed TypedArray with a different size and copy original elements into it.

For example, when you pass, new Uint32Array([1,2,3]) with new size of 5, it will return new Uint32Array([1,2,3,0,0]).

export const resize = <T>(
    source: ArrayLike<T>, newSize: number,
): ArrayLike<T> => {
    if (!source.length) { return new source.constructor(newSize); }
    newSize = typeof newSize === "number" ? newSize : source.length;
    if (newSize >= source.length) {
        const buf = new ArrayBuffer(newSize * source.BYTES_PER_ELEMENT);
        const arr = new source.constructor(buf);
        arr.set(source);
        return arr;
    }
    return source.slice(0, newSize);
};

While the code works as expected, TSC is complaining that 1) ArrayType does not have BYTES_PER_ELEMENT and slice, and 2) Cannot use 'new' with an expression whose type lacks a call or construct signature for the statement new source.constructor().

Is there a way to specify type interfaces for such function that TSC understands my intention?

For 1), I understand ArrayLike does not have interface defined for TypedArray but individual typed array does not seem to inherit from a common class... For instance, instead of using generics, I can use const expand = (source: <Uint32Array|Uint16Array|...>): <Uint32Array|Uint16Array|...> => {}. But it loses context of returning type being same type of the source array.

And for 2) I am clueless on how to tackle this error. It seems reasonable for TSC to complain that source's constructor is lacking type information. But if I can pass proper type for 1), I presume 2) will be disappeared too.

ref) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray

like image 650
shuntksh Avatar asked May 15 '17 20:05

shuntksh


3 Answers

This isn't beautitiful, but it works:

type TypedArray = ArrayLike<any> & {
    BYTES_PER_ELEMENT: number;
    set(array: ArrayLike<number>, offset?: number): void;
    slice(start?: number, end?: number): TypedArray;
};
type TypedArrayConstructor<T> = {
    new (): T;
    new (size: number): T;
    new (buffer: ArrayBuffer): T;
    BYTES_PER_ELEMENT: number;
}

export const resize = <T extends TypedArray>(source: T, newSize: number): T => {
    if (!source.length) {
        return new (source.constructor as TypedArrayConstructor<T>)();
    }

    newSize = typeof newSize === "number" ? newSize : source.length;

    if (newSize >= source.length) {
        const buf = new ArrayBuffer(newSize * source.BYTES_PER_ELEMENT);
        const arr = new (source.constructor as TypedArrayConstructor<T>)(buf);
        arr.set(source);
        return arr;
    }
    return source.slice(0, newSize) as T;
};

(code in playground)

like image 80
Nitzan Tomer Avatar answered Sep 19 '22 23:09

Nitzan Tomer


You could simply define a generic TypedArray in a declaration file.

1- Create a file named extras.d.ts in your app.

2- Add the following line:

type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;

Then the generic TypedArray type will be available throughout your project. You can use this extras.d.ts file to keep declaring more custom types to use throughout your app.

like image 21
Marquizzo Avatar answered Sep 20 '22 23:09

Marquizzo


I grabbed @Nitzan's answer and massaged it until all the type-casts was gone, BUT unfortunately this solution suffers from the untyped constructor property issue here, and there seem to be no workaround without type-casts. I post the code anyway for future reference.

Warning: this code does not compile as of 2017/05/16.

interface GenericTypedArray<T> extends ArrayLike<number> {
    BYTES_PER_ELEMENT: number;
    set(array: ArrayLike<number>, offset?: number): void;
    slice(start?: number, end?: number): T;
    constructor: GenericTypedArrayConstructor<T>;
}

interface GenericTypedArrayConstructor<T> {
    new (): T;
    new (buffer: ArrayBuffer): T;
}

export function resize<T extends GenericTypedArray<T>>(source: T, newSize: number): T {
    if (!source.length) {
        return new source.constructor();
    }

    newSize = typeof newSize === "number" ? newSize : source.length;

    if (newSize >= source.length) {
        const buf = new ArrayBuffer(newSize * source.BYTES_PER_ELEMENT);
        const arr = new source.constructor(buf);
        arr.set(source);
        return arr;
    }
    return source.slice(0, newSize);
};

class DummyArray { 
    constructor();
    constructor(buffer: ArrayBuffer);
    constructor(array: ArrayLike<number>);
    constructor(arg?) { }

    // Hack to have a typed constructor property, see https://github.com/Microsoft/TypeScript/issues/3841
    'constructor': typeof DummyArray;
    BYTES_PER_ELEMENT: number;
    length: number;
    [index: number]: number;

    set(array: ArrayLike<number>, offset?: number): void { }
    slice(start?: number, end?: number): this { return this; }

    static BYTES_PER_ELEMENT: number;
}

// How it intended to work
resize(new DummyArray([1, 2, 3]), 5);

// How it fails to typecheck
// Types of property 'constructor' are incompatible.
//   Type 'Function' is not assignable to type 'GenericTypedArrayConstructor<Uint8Array>'.
resize(new Uint8Array([1, 2, 3]), 5); 
like image 23
Tamas Hegedus Avatar answered Sep 17 '22 23:09

Tamas Hegedus