Incompatible with TypeScript 5.9+
Note the recent changes to Uint8Array mentioned in the TypeScript 5.9 docs:
In order for the decoded data to be usable without casting, type definitions should be adjusted to:
-
decode: (str: string) => Uint8Array<ArrayBuffer> - optionally broaden the accepted param to
encode: (data: AllowSharedBufferSource) => string
What do you mean "incompatible"? Specific example? We compile with typescript 5.9.
https://github.com/paulmillr/scure-base/blob/f948c5a540d53f09291a37f16a29549e2c376180/package.json#L18
Using Uint8Array<ArrayBuffer> will make the library unusable for typescript 5.5 users which does not have uint8array generics.
Of course it compiles just fine, as long as you don't use decoded bytes anywhere. With 5.9, core library functions became more explicit about whether they can consume a view or need a self-contained buffer. Any attempt to pass a Uint8Array to a parameter expecting an Uint8Array<ArrayBuffer> causes a compilation failure.
Can you provide a specific example?
All noble libraries and other stuff except for DOM built-in garbage can consume generic Uint8array without hassle.
I guess I'm relying on "DOM built-in garbage", as I'm building for the web:
crypto.subtle.importKey('pkcs8', base64.decode("..."), ...)
Typescript 5.5 and earlier users (lots of them) will see compilation errors if the library switches to generics.
I see. It's an annoying language change... So I guess no chance to adapt without a major release?
I have some ideas.
type Bytes = ReturnType<typeof Uint8Array.of>;
decode(): Bytes
Typescript did a really terrible job, making this change in minor release. So many users will get bitten. Also maintainers of software which needs to support more than ts 5.9.
Interesting approach. If this works, it might be worth spreading this idea to other lib maintainers as well.
Do I understand correctly that the problem is crypto.subtle.importKey taking a type BufferSource = ArrayBufferView<ArrayBuffer> | ArrayBuffer; and now Uint8Array is not an ArrayBuffer anymore?
In that case the solution is probably
-crypto.subtle.importKey('pkcs8', base64.decode("..."), ...)
+crypto.subtle.importKey('pkcs8', base64.decode("...").buffer, ...)
as suggested in the linked docs
Do I understand correctly that the problem is
crypto.subtle.importKeytaking atype BufferSource = ArrayBufferView<ArrayBuffer> | ArrayBuffer;and nowUint8Arrayis not anArrayBufferanymore?
Yes, but keep in mind importKey is just an example. Many APIs from lib.dom.d.ts take a BufferSource.
The problem seems to be that Uint8Array defaults to Uint8Array<ArrayBufferLike>, not Uint8Array<ArrayBuffer>.
In that case the solution is probably [using
.buffer]
Two problems with .buffer:
-
base64.decode(payload.key).bufferyields anArrayBufferLike, which is still not assignment compatible withArrayBuffer(and thusBufferSource) - it breaks when dealing with views, e.g.
new Uint8Array([1, 2, 3, 4, 5]).subarray(2, 3).buffer
So far the safest bet seems to call .slice() or explicitly casting when you know the type.
You can also create a fit function like that which does not perform a copy in the majority of use cases and avoids an potentially unsafe cast:
function fit<T extends ArrayBufferLike>(source: Uint8Array<T>): Uint8Array<ArrayBuffer> {
const buffer = source.buffer;
if (buffer instanceof ArrayBuffer) {
return new Uint8Array(buffer); // new instance just to make TS happy without unsafe cast
}
const copy = new ArrayBuffer(buffer.byteLength);
new Uint8Array(copy).set(new Uint8Array(buffer));
return new Uint8Array(copy);
}
let bytesFromLib = fromHex("AA"); // returns newly created `Uint8Array`
let destinationApi: Uint8Array<ArrayBuffer> = fit(bytesFromLib);
(in this example fromHex has the same role as base64.decode, a Uint8Array constructor)
Since I don't agree with those TS changes, opened a ts issue in august, there is some discussion going on:
https://github.com/microsoft/TypeScript/issues/62240
So far the safest bet seems to call
.slice()
Note that .slice() behavior is significantly different in Uint8Array and Buffer which extends Uint8Array
> x = Uint8Array.of(1); x.slice()[0]=42; x[0]
1
> x = Buffer.of(1); x.slice()[0]=42; x[0]
42
Using .slice() on input is usually the wrong choice because of this.
Ether .subarray for no-copy or explicit Uint8Array.prototype.slice for copy is fine.
Using .slice() on explicitly constructed Uint8Array instances is fine, this problem only affects input
But on output you likely don't want .slice() for perf