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
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 TypedArray
s 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With