Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does TypeScript implicitly convert Uint8Array to Float32Array

Tags:

typescript

Why does TypeScript not throw an error when a Uint8Array is passed into a function expecting an ArrayBuffer? I assume it must be implicitly convertible, but if you use it later it results in incorrect behaviour. For example in my case I was converting it to a Float32Uint but it cast each element instead of mapping it. Is there any way to get TypeScript to catch this sort of error.

function test(data: ArrayBuffer)
{
  console.log("Bytelength " + data.byteLength);
  let view = new Float32Array(data);
  console.log("Size of Float32Array " + view.length);
}

var uint_array : Uint8Array = new Uint8Array(4);

console.log("Test passing wrong type");
test(uint_array);
console.log("Test passing correct type");
test(uint_array.buffer);

Output is:

Test passing wrong type
Bytelength 4
Size of Float32Array 4
Test passing correct type
Bytelength 4
Size of Float32Array 1

Edited to add a simpler example to show the issue. I pass in a type that is clearly wrong. The log outputs the wrong type, but TypeScript is fine with it.

function test(data: ArrayBuffer)
{
    console.log("Type of data " + data.constructor.name);
}

var uint_array : Uint8Array = new Uint8Array(4);

test(uint_array); 
test(uint_array.buffer);

> Type of data Uint8Array
> Type of data ArrayBuffer
like image 492
mattking Avatar asked Oct 19 '25 19:10

mattking


1 Answers

Yes, the issue is that TypeScript's type system is structural and not nominal, so you can assign a Uint8Array to something that expects an ArrayBuffer if the structure of ArrayBuffer is satisfied by Uint8Array. It has nothing to do with the type names or declaration sites.

The definitions for Uint8Array and ArrayBuffer are split among a lot of different declaration files, but the main one is lib.es5.d.ts.

The definition of ArrayBuffer is

interface ArrayBuffer {
    readonly byteLength: number;
    slice(begin: number, end?: number): ArrayBuffer;
}

and the definition of Uint8Array contains

interface Uint8Array {
    // ...
    readonly buffer: ArrayBufferLike;
    // ...
    readonly byteLength: number;
    // ...      
    slice(start?: number, end?: number): Uint8Array;
    // ...
}

And so the compiler sees Uint8Array as assignable to ArrayBuffer. That's just how it is.


This was reported as microsoft/TypeScript#31331 which was closed as a duplicate of microsoft/TypeScript#202 which is the catch-all issue for providing a way to support nominal typing. You only want something to be a valid ArrayBuffer if it is declared to be, not if it happens to match the structure.


There are workarounds which simulate nominal typing. One approach is to merge a member into the ArrayBuffer interface to make TypedArrays incompatible with it. This is called "branding". Such a brand property doesn't really exist at runtime, it's just a way for TypeScript to tell the difference between ArrayBuffer and otherwise-compatible things:

// merge a brand property
interface ArrayBuffer {
  __ArrayBufferBrand: true;
}

And that prevents the bad call:

console.log("Test passing wrong type");
test(uint_array); // error! 
// Argument of type 'Uint8Array' is not assignable to parameter of type 'ArrayBuffer'.

Hooray! But it also prevents the good call:

console.log("Test passing correct type");
test(uint_array.buffer); // error!
// Argument of type 'ArrayBufferLike' is not assignable to 
// parameter of type 'ArrayBuffer'.
 

Boo! The problem is that the buffer property of uint_array is typed as ArrayBufferLike which is not exactly ArrayBuffer. ArrayBufferLike is defined like

type ArrayBufferLike = ArrayBufferTypes[keyof ArrayBufferTypes];

interface ArrayBufferTypes {
    ArrayBuffer: ArrayBuffer;
}

which also gets merged into from SharedArrayBuffer in another declaration file.

So we need to make sure that ArrayBufferLike is assignable to ArrayBuffer, meaning we need to also merge that brand property into SharedArrayBuffer:

interface SharedArrayBuffer {
  __ArrayBufferBrand: true;
}

And now everything works the way we want:

console.log("Test passing wrong type");
test(uint_array); // error! 
// Argument of type 'Uint8Array' is not assignable to parameter of type 'ArrayBuffer'.
console.log("Test passing correct type");
test(uint_array.buffer); // okay

Hooray again!


But that's a lot of effort to prevent this problem, and if ArrayBufferLike ever gets more members in it, you might have to manually merge into those members. And maintaining this yourself might be more trouble than it's worth. I guess it's up to you.

Playground link to code

like image 165
jcalz Avatar answered Oct 21 '25 10:10

jcalz