Typescript: Suggestion: a built-in TypedArray interface

Created on 26 Apr 2017  路  18Comments  路  Source: microsoft/TypeScript

I would have expected a TypedArray interface to exist in
the built-in declaration libraries. Instead, there are independent types for
Int8Array, etc, with no common base type.

interface Int8Array { /* ... */ }
interface Int8ArrayConstructor { /* ... */ }
declare const Int8Array: Int8ArrayConstructor;

interface Uint8Array { /* ... */ }
interface Uint8ArrayConstructor { /* ... */ }
declare const Uint8Array: Uint8ArrayConstructor;

// ...

It seems sensible that there should be a common TypedArray type, both because

  • a TypeScript user might want to declare a variable as a TypedArray, and
  • it would reduce repetition in the built-in declaration files
interface TypedArray { /* ... */ }

interface Int8Array extends TypedArray { }
interface Int8ArrayConstructor { /* ... */ }
declare const Int8Array: Int8ArrayConstructor;

interface Uint8Array extends TypedArray { }
interface Uint8ArrayConstructor { /* ... */ }
declare const Uint8Array: Uint8ArrayConstructor;

// ...

Is there a good reason why there is no TypedArray type? If not, I can submit a PR.

Use case

Consider the isTypedArray() function from lodash.
Currently, it is declared as follows:

function isTypedArray(value: any): boolean;

This proposal would enable an appropriate type-guard, without declaring
a convoluted union type:

function isTypedArray(value: any): value is TypedArray;
lib.d.ts Suggestion help wanted

Most helpful comment

I seriously don't see a problem with introducing an interface called TypedArray.
As of the specifications (e.g. for Float64Array "All Float64Array objects inherit from %TypedArray%.prototype."), all (aforementioned) numerically typed arrays derive from the Prototype of the TypedArray class. Meaning: They share the same public TypedArray properties and methods, hence they share the same public interface.

To enable a TypeScript-native (unhacky) abstraction for numerically typed arrays, I strongly recommend the introduction of a TypedArray interface. This would be sufficient enough to handle the aforementioned issues and would obviously comply to the Specifications and finally ensure much less and cleaner code.

All 18 comments

You can achieve the same behavior today by defining:

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

but in general define one TypedArray interface and use this types should reduce the duplication.

@mhegazy Yes, that's the solution I'm using now, but it seems a bit unnecessary. I'll submit a PR shortly.

Would asking for types for 0x, 0b, and 0o be a different request on this? The reason I would like these type is to use with TypedArrays. Currently there is no guard on inserting any number into those typedArrays. Which shouldn't be allowed. Lets say you wanted to put a value 255 into a Int8Array field, this should have a type guard for

type int8 = -0x80 to 0x7F | -0b10000000 to 0b1111111 | -0o200 to 0o177
type hex = any 0xFFFFF...;
type binary = any 0b1001010101...;
type octal = any 0o17235...;

Something that would prevent a programmer from inserting numbers that will be converted within the typed arrays.

@marcusjwhelan That is a very different proposal, and far beyond the scope of this one. This is just about adding another interface to the declaration files, not about introducing new syntax.

There already is ArrayBufferView interface in lib.d.ts for this exact purpose, which is a supertype of all typed arrays.

interface ArrayBufferView {
    /**
      * The ArrayBuffer instance referenced by the array.
      */
    buffer: ArrayBuffer;

    /**
      * The length in bytes of the array.
      */
    byteLength: number;

    /**
      * The offset in bytes of the array.
      */
    byteOffset: number;
}

interface ArrayBufferConstructor {
    readonly prototype: ArrayBuffer;
    new (byteLength: number): ArrayBuffer;
    isView(arg: any): arg is ArrayBufferView;
}
declare const ArrayBuffer: ArrayBufferConstructor;

ArrayBuffer.isView(new Uint8Array()); // true

On Web IDL:

typedef (Int8Array or Int16Array or Int32Array or
         Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
         Float32Array or Float64Array or DataView) ArrayBufferView;

The solution that @mhegazy gives might be okay in some cases. But I'm writing code that is something like this:

type Arr<T> = T[] | Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;

function isSame<T>(first: Arr<T>, second: Arr<T>) {
  return first.every((ele: T, index: number): boolean => {
    return ele === second[index];
  });
}

And it just will not compile because every has this type def (it is copied for each type of array):

every(callbackfn: (this: void, value: number, index: number, array: Float64Array) => boolean): boolean;

FWIW, I have this concept in my library as I have to deal with TypedArrays a lot, see:
https://github.com/stardazed/stardazed/blob/master/src/core/array.ts#L36
I split it into a base and mutable and readonly extended interfaces, code as added works only in 2.4+ as the SharedArrayBuffer type was modified in the std declarations.

I also made a TS fork of gl-matrix (https://github.com/stardazed/veclib) where I often had to deal with TypedArray or array inputs being returned and wanting TS to deduce the right return type, solved like so:

type AN = ArrayOfNumber; // an ArrayLike<number> with non-readonly subscript
type ACN = ArrayOfConstNumber; // ArrayLike<number>

export function min(out: number[], a: ACN, b: ACN): number[];
export function min<T extends AN>(out: T, a: ACN, b: ACN): T;
export function min(out: AN, a: ACN, b: ACN) { ...code... }

This allows it to return the correct type info for both:

const a = min([], [1,2,3], [3,2,1]); // a: number[]
const b = min(new Int32Array(3), [1,2,3], [3,2,1]); // b: Int32Array

It seems like @saschanaz has the correct answer, and this issue can be closed

Wait, just kidding:

Property 'length' does not exist on type 'ArrayBufferView'.

Unfortunately, I think adding an interface for TypedArray might be a bad idea. TypedArray is a weird JavaScript constructor, which is why I think TypeScript is correct to avoid implementing a regular interface for it. As an alternative, I suggest adding length: number; to the ArrayBufferView interface.

I found this thread after I read about TypedArrays on MDN then tried to write instanceof TypedArray, which gave me a TypeScript error. After (erroneously) thinking this was a problem with TypeScript, I tried the same statement in plain JavaScript, and promptly got a JavaScript error too.

However, TypedArray _is_ a constructor, it's just not (usually) a _visible_ one, which is why new and instanceof won't work with it. To test this, I created a new Int16Array() in Chrome, and checked its prototype chain (logging count, Object.prototype.toString.call(obj), obj.constructor.name, obj):

1 "[object Object]" "Int16Array" TypedArray {constructor: 茠, BYTES_PER_ELEMENT: 2}
2 "[object Object]" "TypedArray" {constructor: 茠,聽鈥
3 "[object Object]" "Object" {constructor: 茠, __defineGetter__: 茠, __defineSetter__: 茠, hasOwnProperty: 茠, __lookupGetter__: 茠,聽鈥

According to MDN "There is no global property named TypedArray, nor is there a directly visible TypedArray constructor." (I admit I don't fully understand what it means to be an "invisible" constructor. I understand why JavaScript doesn't allow new TypedArray, because TypedArray only exists to centralize the common properties of a family of related constructors, and therefor shouldn't be instantiated by itself. But why disallow checking instanceof TypedArray? TypedArray clearly _is_ in the prototype chain. Is it not possible for JavaScript to disallow one without the other?)

I might argue that it would make more sense for JavaScript to implement TypedArray as an normal constructor, the wayBlob is File's parent, but also a normal constructor itself. Here's the prototype chain for a new File():

1 "[object File]" "File" File {constructor: 茠,聽鈥
2 "[object Blob]" "Blob" Blob {slice: 茠, constructor: 茠, Symbol(Symbol.toStringTag): "Blob"}
3 "[object Object]" "Object" {constructor: 茠, __defineGetter__: 茠, __defineSetter__: 茠, hasOwnProperty: 茠, __lookupGetter__: 茠,聽鈥

So Blob is to File as TypedArray is to Int16Array, but because Blob is a normal constructor, it's much more intuitive to work with. Running instanceof TypedArray on a Int16Array results in an error, whereas running instanceof Blob on a File returns true, exactly as you would expect.

However, until JavaScript makes TypedArray a normal constructor, I don't think TypeScript should implement an interface for it. Doing so might confuse users, who would then expect TypedArray to behave like other JavaScript constructors, which it does not.

As an alternative, I recommend adding length: number; to the existing ArrayBufferView interface. This small change would give users a single, complete, generic interface they could use for all TypedArrays, while reenforcing (because of its different name) that it is just a TypeScript interface, and should _not_ be used as a JavaScript constructor.

I get why we should not write something like UIntArray extends TypedArray because there is no constructor but why should we avoid adding an interface for it? An interface does not require a constructor.

Writing something like UIntArray implements TypedArray does not automatically mean that we can call new TypedArray().

Using a common interface for TypedArrays would remove redundant declaration and would not imply that TypedArray is a constructor i.e. can be used for instanceof. It would also help in several type definitions that require TypedArray as a type e.g node type declarations.

ArrayBufferView might be a solution but I'm not familiar with Web IDL and I never seen this type before. I always saw DataView | TypedArray. It is also currently defined as an interface in lib.d.ts and not as a union type as described in the ArrayBufferView doc.

ArrayBufferView has no property length! This is a bug, right? Specs
Only byteLength is available. On a Int16Array(2) length is 2 and byteLength 4.
length just returns the count of elements. Very important after array buffer import.

What about a common interface for typed and untyped arrays? TypeableArray<T>

I have already published this type. You can take a look at the project page: typeable-array.

@saschanaz does not work. It lacks most functionality that a typed array would have such as length or even member access. The reason is that this interface is meant to be used for any view of arraybuffer. So DataView is also included.

I seriously don't see a problem with introducing an interface called TypedArray.
As of the specifications (e.g. for Float64Array "All Float64Array objects inherit from %TypedArray%.prototype."), all (aforementioned) numerically typed arrays derive from the Prototype of the TypedArray class. Meaning: They share the same public TypedArray properties and methods, hence they share the same public interface.

To enable a TypeScript-native (unhacky) abstraction for numerically typed arrays, I strongly recommend the introduction of a TypedArray interface. This would be sufficient enough to handle the aforementioned issues and would obviously comply to the Specifications and finally ensure much less and cleaner code.

@dschnelldavis new doesn't work because it's essentially an abstract constructor, for lack of a better terminology. However, instanceof _does_ work as demonstrated in this snippet:

const TypedArray = Object.getPrototypeOf(Uint8Array)
console.log(new Uint8Array() instanceof TypedArray)

As other people have said, even though it's an invisible _constructor_, there should still be an _interface_ for it, as it encapsulates a lot of functionality in a single base class and drastically reduces repetition for definition files, as well as more accurately reflecting the specification.

I think there should also be an interface for TypedArrayConstructor as well, since there are common static properties, so something like this:

interface TypedArray {
  ...
}

interface TypedArrayConstructor {
  readonly prototype: TypedArray;
  ...
}

interface Float32Array extends TypedArray { }
interface Float64Array extends TypedArray { }
interface Int16Array extends TypedArray { }
interface Int32Array extends TypedArray { }
interface Int8Array extends TypedArray { }
interface Uint16Array extends TypedArray { }
interface Uint32Array extends TypedArray { }
interface Uint8Array extends TypedArray { }
interface Uint8ClampedArray extends TypedArray { }

interface Float32ArrayConstructor extends TypedArrayConstructor { }
interface Float64ArrayConstructor extends TypedArrayConstructor { }
interface Int16ArrayConstructor extends TypedArrayConstructor { }
interface Int32ArrayConstructor extends TypedArrayConstructor { }
interface Int8ArrayConstructor extends TypedArrayConstructor { }
interface Uint16ArrayConstructor extends TypedArrayConstructor { }
interface Uint32ArrayConstructor extends TypedArrayConstructor { }
interface Uint8ArrayConstructor extends TypedArrayConstructor { }
interface Uint8ClampedArrayConstructor extends TypedArrayConstructor { }

Has there been progress along this avenue?

Another example - node has fs.writeFile which accepts data prop which can be TypedArray. However, in @types/node types data is defined as any. Same with many other libraries using fs inside.

It would be really nice to have this one and change any to proper types.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

weswigham picture weswigham  路  3Comments

manekinekko picture manekinekko  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

uber5001 picture uber5001  路  3Comments

Zlatkovsky picture Zlatkovsky  路  3Comments