📝

TS 5.7 から ArrayBufferView(各 TypedArray と DataView)の型がジェネリクスになりました

2024/09/29に公開
変更情報

【2024/11/29】

  • 正式版がリリースされたため、一部記述を修正しました

【2024/10/07】

  • 注釈を修正
  • 型の記述をわかりやすくした

【2024/10/02】

  • タイトルに TS 5.7 を追加

【2024/09/30】

  • DefinitelyTyped について追記

結論

次のバージョンである TypeScript 5.7 で ArrayBufferView(各 TypedArrayDataView)の型がジェネリクスになり、ArrayBufferSharedArrayBuffer のどっちを保持しているのかを型レベルで判定できるようになります。

https://github.com/microsoft/TypeScript/pull/59417

Uint8Array の型について一部抜き出してみると以下のようになります。

type ArrayBufferLike = ArrayBuffer | SharedArrayBuffer;

interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {
  readonly buffer: TArrayBuffer;
  // ...

  slice(start?: number, end?: number): Uint8Array<ArrayBuffer>;
  subarray(begin?: number, end?: number): Uint8Array<TArrayBuffer>;
  // ...
}

interface Uint8ArrayConstructor {
  readonly prototype: Uint8Array<ArrayBufferLike>;
  // ...

  new (): Uint8Array<ArrayBuffer>;
  new (length: number): Uint8Array<ArrayBuffer>;
  new (array: ArrayLike<number>): Uint8Array<ArrayBuffer>;
  new (elements: Iterable<number>): Uint8Array<ArrayBuffer>;
  new <TArrayBuffer extends ArrayBufferLike = ArrayBuffer>(
    buffer: TArrayBuffer,
    byteOffset?: number,
    length?: number,
  ): Uint8Array<TArrayBuffer>;
  // ...
}

ライブラリなどで型情報をパブリッシュしている場合は対応が必要そうです。なお DefinitelyTyped は一括で対応されました(package.json の typesVersions で対応されているため、実質別の方ファイルであると思わなくもない)。

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/70390

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/70694

背景

ES2023 まで ArrayBufferSharedArrayBuffer は同じ構造

TypeScript は型システムに構造的型付けを採用しています。名前が異なっていても構造が一緒の場合同一の型として扱います。

ところで ES2023 まで ArrayBufferSharedArrayBuffer は構造が一緒です。実際 TypeScript 5.6 で ES2023 (ESNext) をターゲットにした場合、ArrayBuffer 型が期待されている変数に SharedArrayBuffer を代入できます[1]

ES2023 ターゲットで型エラーを起こさない
const buffer: ArrayBuffer = new SharedArrayBuffer(256);

https://www.typescriptlang.org/play/?target=10&ts=5.6.2#code/MYewdgzgLgBARgVwGZIKYCcBcMCC70CGAngELJrowC8MYqA7jAMoAWB6qAJnoaeRgAoATAFYAbAEoA3ACggA

これによって ArrayBufferViewbuffer プロパティが ArrayBufferLikeArrayBufferSharedArrayBuffer のどっちを保持しているかわからない)であるにも関わらず、チェックが緩くなり型エラーが出ていない状況になっていました。

TypeScript 5.6 まで
const bytes = new Uint8Array(256); // 型で ArrayBuffer を保持している情報が抜け落ちている
const hash = crypto.subtle.digest(
  "SHA-512",
  bytes.buffer, // bytes.buffer は ArrayBufferLike だが型エラーにならない
);

ES2024 の型導入にあたって問題が発覚

さて ES2024 として仕様に入った Resizable and growable ArrayBuffers と ArrayBuffer Transfer によって ArrayBufferSharedArrayBuffer がそれぞれ独立してプロパティやメソッドを持つようになり、構造が一致しなくなりました。

https://zenn.dev/pixiv/articles/473c0f9bf546cd

ES2024 の型を TypeScript に組み込むにあたって npm の top 400 のライブラリでチェックした際に広く型エラーを起こしてしまうことが発覚し、先程の問題が顕在化しました。

これは ES2023 まで ArrayBufferSharedArrayBuffer が同じ構造なのも要因の一つではありますが、主な原因は ArrayBufferViewbuffer プロパティが ArrayBufferLike になっていることです。そこで ArrayBufferView をジェネリクスにし、ArrayBufferSharedArrayBuffer のどっちを保持しているのか型レベルでわかるようにし解決されることになりました。

TypeScript 5.7 以降
const bytes = new Uint8Array(256); // Uint8Array<ArrayBuffer>
const hash = crypto.subtle.digest(
  "SHA-512",
  bytes.buffer, // bytes.buffer が ArrayBuffer として扱われ、型エラーにならない
);

余談

以前から TypedArray インターフェースを導入しプロパティやメソッドを共通化することが要望されています。この変更については今回見送られました。

https://github.com/microsoft/TypeScript/pull/59205

脚注
  1. 互換性の理由から ArrayBufferSymbol.species プロパティが文字列リテラル型ではなく string として定義されており、逆は成り立たないみたいです。これについては ArrayBufferSharedArrayBuffer が混ざらないように自分が別の PR を出しています↩︎

Discussion