scure-base icon indicating copy to clipboard operation
scure-base copied to clipboard

Incompatible with TypeScript 5.9+

Open overheadhunter opened this issue 4 months ago • 13 comments

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

overheadhunter avatar Oct 17 '25 13:10 overheadhunter

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.

paulmillr avatar Oct 17 '25 14:10 paulmillr

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.

overheadhunter avatar Oct 17 '25 14:10 overheadhunter

Can you provide a specific example?

All noble libraries and other stuff except for DOM built-in garbage can consume generic Uint8array without hassle.

paulmillr avatar Oct 17 '25 14:10 paulmillr

I guess I'm relying on "DOM built-in garbage", as I'm building for the web:

crypto.subtle.importKey('pkcs8', base64.decode("..."), ...)

overheadhunter avatar Oct 17 '25 14:10 overheadhunter

Typescript 5.5 and earlier users (lots of them) will see compilation errors if the library switches to generics.

paulmillr avatar Oct 17 '25 14:10 paulmillr

I see. It's an annoying language change... So I guess no chance to adapt without a major release?

overheadhunter avatar Oct 17 '25 14:10 overheadhunter

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.

paulmillr avatar Oct 17 '25 15:10 paulmillr

Interesting approach. If this works, it might be worth spreading this idea to other lib maintainers as well.

overheadhunter avatar Oct 17 '25 16:10 overheadhunter

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

Image

webmaster128 avatar Oct 22 '25 08:10 webmaster128

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?

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:

  1. base64.decode(payload.key).buffer yields an ArrayBufferLike, which is still not assignment compatible with ArrayBuffer (and thus BufferSource)
  2. 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.

overheadhunter avatar Oct 23 '25 06:10 overheadhunter

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)

webmaster128 avatar Oct 23 '25 07:10 webmaster128

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

paulmillr avatar Oct 23 '25 12:10 paulmillr

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

ChALkeR avatar Oct 24 '25 16:10 ChALkeR