better-typescript-lib icon indicating copy to clipboard operation
better-typescript-lib copied to clipboard

structuredCloneについて

Open KisaragiEffective opened this issue 1 year ago • 9 comments

structuredCloneについてこの型定義ライブラリでオーバーライドされていると便利かなと思うのですがいかがでしょうか?標準ライブラリのそれは今の所<T = any>(val: T, options?: StructuredSerializeOptions): Tというシグネチャになっており、うっかりすると実行時エラーを引き起こしそうです (例えば殆どのクラスはコンストラクタの情報を暗に持つためシリアライズできず、例外が起きます)。

提案するだけでは忍びないので実際にたたき台を作ってみました。継承が保存されずに親クラスに弱化されることに注意すると読みやすいと思います。

type StructuredCloneOutput<T> = T extends never 
  ? never 
  : /* T extends EvalError // TS is not nominal...
    // ? EvalError
    :*/ T extends RangeError
      ? RangeError
      : T extends ReferenceError
        ? ReferenceError
        : T extends SyntaxError
          ? SyntaxError
          : T extends TypeError
            ? TypeError
            : T extends URIError
              ? URIError
              : /*
                T extends AggregateError
                ? AggregateError
                :
                */
                T extends DOMException
                ? DOMException
                :
                T extends Error
                ? Error // weaken (constructor)
                : T extends boolean
                  ? T // リテラル型
                  : T extends string
                    ? T // リテラル型
                    : T extends [...any]
                    ? T
                    : T extends Array<infer R>
                      ? Array<R> // weaken (constructor)
                      : T extends null
                        ? null
                        : T extends undefined
                          ? undefined
                          : T extends Map<infer K, infer V>
                            ? Map<K, V>
                            : T extends Set<infer E>
                              ? Set<E>
                              : T extends number
                                ? number
                                : T extends bigint
                                  ? T // literal
                                  : T extends number
                                    ? T
                                    : T extends symbol
                                      ? never // symbol cannot be cloned
                                      : T extends Boolean
                                        ? Boolean
                                        : T extends String
                                          ? String
                                          : T extends Date
                                            ? Date
                                            : T extends RegExp
                                              ? RegExp
                                              : T extends Blob
                                                ? Blob
                                                : T extends File
                                                  ? File
                                                  : T extends FileList
                                                    ? FileList
                                                    : T extends ArrayBuffer
                                                      ? ArrayBuffer
                                                      : T extends Int8Array
                                                        ? Int8Array // weaken (constructor)
                                                        : T extends Int16Array
                                                          ? Int16Array // weaken (constructor)
                                                          : T extends Int32Array
                                                            ? Int32Array // weaken (constructor)
                                                            : T extends BigInt64Array
                                                              ? BigInt64Array // weaken (constructor)
                                                              : T extends Uint8Array
                                                                ? Uint8Array // weaken (constructor)
                                                                : T extends Uint16Array
                                                                  ? Uint16Array // weaken (constructor)
                                                                  : T extends Uint32Array
                                                                    ? Uint32Array // weaken (constructor)
                                                                    : T extends BigUint64Array
                                                                      ? BigUint64Array // weaken (constructor)
                                                                      : T extends Float32Array
                                                                        ? Float32Array // weaken (constructor)
                                                                        : T extends Float64Array
                                                                          ? Float64Array // weaken (constructor)
                                                                          : T extends Uint8ClampedArray
                                                                            ? Uint8ClampedArray // weaken (constructor)
                                                                            : T extends DataView
                                                                              ? DataView
                                                                              : T extends ImageBitmap
                                                                                ? ImageBitmap
                                                                                : T extends ImageData
                                                                                  ? ImageData
                                                                                  : T extends Function
                                                                                  ? never
                                                                                  : T extends object 
                                                                                    ? { [K in keyof T]: StructuredCloneOutput<T[K]> } // value is writable
                                                                                    : never // T is symbol, it is not structured-cloneable

declare function structuredClone<T>(val: T, options?: StructuredSerializeOptions): StructuredCloneOutput<T>;

KisaragiEffective avatar Jan 28 '24 12:01 KisaragiEffective

脳筋な実装をしたのは~~それしか知らなかったから~~と型名がホバーした時にわかりやすくなるからという利点を選択したからですが、メンテナンス性には欠けるかもしれません。

KisaragiEffective avatar Jan 28 '24 12:01 KisaragiEffective

提案ありがとうございます。structuredCloneの型をより安全にするのはよいアイデアだと思いますが、 さすがにこの実装だとパワーがありすぎるのでどんな実装ができそうか検討します🤔

uhyo avatar Jan 30 '24 05:01 uhyo

TypeScript本体の型定義では、次の手法が紹介されていました。

type Cloneable<T> = T extends Function | Symbol
  ? never 
  : T extends Record<any, any> 
    ? {-readonly [k in keyof T]: Cloneable<T[k]>}
    : T

declare function structuredClone<T>(value: Cloneable<T>, options?: StructuredSerializeOptions | undefined): Cloneable<T>

これはメンテナンス性が高い一方、本来削ぎ落とさなければならないプロパティが存在したままになってしまいます:

class MyError extends Error {
    public hi: string | undefined = "";
}

// TS2339。プロパティが存在しない。
const y = structuredClone(new Error()).hi;
//    ^?
// TSではエラーにならない。しかし実行時にstructuredCloneはErrorを返すので実行時エラー。
const test = structuredClone(new MyError()).hi;
//    ^?

妥当な案としては{ [k in Pick<keyof T, keyof Error>]: Cloneable<T[k]> }のようにすることもできそうですが、これもまたインデントが項目数の二乗に比例してしまいそうです。

KisaragiEffective avatar Jan 30 '24 06:01 KisaragiEffective

type Unit = [];
type Weaken = [
    RangeError, ReferenceError, TypeError, SyntaxError, URIError, Error, Boolean, String, Date, RegExp, Blob, File, FileList,
    Int8Array, Int16Array, Int32Array, BigInt64Array, Uint8Array, Uint16Array, Uint32Array, BigUint64Array, Uint8ClampedArray,
    DataView, ImageBitmap, ImageData
];

type WeakenN<PI extends readonly [...Unit[]]> = Weaken[PI["length"]];
type StructuredCloneOutput<T> = RecurseHelper<T, Unit>
type RecurseHelper<T, PI extends readonly [...Unit[]]> = 
    T extends Function | Symbol 
        ? never
        : T extends Map<infer K, infer V> // weaken
        ? Map<K, V>
        : T extends Set<infer E> // weaken
        ? Set<E>
        : T extends Record<any, any>
            ? T extends WeakenN<PI>
                ? WeakenN<PI>
                : PI["length"] extends Weaken["length"]
                    ? {-readonly [k in keyof T]: StructuredCloneOutput<T[k]>}
                    : RecurseHelper<T, [...PI, Unit]>
            : T

declare function structuredClone<const T>(value: T, options?: StructuredSerializeOptions | undefined): StructuredCloneOutput<T>

ということであれやこれややって、インデントの二乗に比例しないようなコードを得ることができたと思います。 MapSetについては型引数があるのでWeakenに入れることが難しそうです。

KisaragiEffective avatar Jan 30 '24 07:01 KisaragiEffective

TS自体の再帰制限がそれほど変わらなかったので、構文上で再帰を行わない実装にしました。 また、MapとSetについてStructuredCloneOutputを噛ませ、テストを追加するとこんな感じになりました。

コード
type Basics = [RangeError, ReferenceError, TypeError, SyntaxError, URIError, Error, Boolean, String, Date, RegExp]
type DOMSpecifics = [
    DOMException,
    DOMMatrix,
    DOMMatrixReadOnly,
    DOMPoint,
    DOMPointReadOnly,
    DOMQuad,
    DOMRect,
    DOMRectReadOnly,
]
type FileSystemTypeFamily = [
    FileSystemDirectoryHandle,
    FileSystemFileHandle,
    FileSystemHandle,
]
type WebGPURelatedTypeFamily = [
    // GPUCompilationInfo,
    // GPUCompilationMessage,
]
type TypedArrayFamily = [
    Int8Array, Int16Array, Int32Array, BigInt64Array, Uint8Array, Uint16Array, Uint32Array, BigUint64Array, Uint8ClampedArray,
]
type Weaken = [
    ...Basics,
    // AudioData,
    Blob,
    // CropTarget,
    // CryptoTarget,
    ...DOMSpecifics,
    ...FileSystemTypeFamily,
    ...WebGPURelatedTypeFamily,
    File, FileList,
    ...TypedArrayFamily,
    DataView, ImageBitmap, ImageData,
    RTCCertificate,
    VideoFrame,
];

type MapSubtype<R> = {[k in keyof Weaken]: R extends Weaken[k] ? true : false};
type SelectNumericLiteral<H> = number extends H ? never : H;
type FilterByNumericLiteralKey<R extends Record<string | number, any>> = {[
    k in keyof R as `${R[k] extends true ? Exclude<SelectNumericLiteral<k>, symbol> : never}`
]: []};
type HitWeakenEntry<E> = keyof FilterByNumericLiteralKey<MapSubtype<E>>;

type StructuredCloneOutput<T> = 
    T extends Function | Symbol 
        ? never
        : T extends Map<infer K, infer V>
        ? Map<StructuredCloneOutput<K>, StructuredCloneOutput<V>>
        : T extends Set<infer E>
        ? Set<StructuredCloneOutput<E>>
        : T extends Record<any, any>
            ? HitWeakenEntry<T> extends never
                ? {-readonly [k in keyof T]: StructuredCloneOutput<T[k]>}
                // hit
                : Weaken[HitWeakenEntry<T>]
            : T

declare function structuredClone<const T>(
    value: T, options?: StructuredSerializeOptions | undefined
): StructuredCloneOutput<T>

class Weirdo extends Int16Array {
    public weirdo: undefined = undefined;
}

class Weirdo2 extends Int32Array {
    public weirdo2: undefined = undefined;
}

const a: 1 = structuredClone(1);
const b: Int16Array = structuredClone(new Int16Array());
// @ts-expect-error property do not exist
const c: undefined = structuredClone(new Weirdo()).weirdo;
const f = [new Weirdo()] as const;
const g: [Int16Array] = structuredClone(f);
const h = new Map([[new Weirdo(), new Weirdo2()]]);
const i: Map<Int16Array, Int32Array> = structuredClone(h);
// @ts-expect-error weaken types
const i_: Map<Weirdo, Weirdo2> = structuredClone(h);
const j = new Set([new Weirdo()]);
const k: Set<Int16Array> = structuredClone(j);
// @ts-expect-error weaken type
const k_: Set<Weirdo> = structuredClone(j);
// not cloneable
const m: never = structuredClone(class {});
// not cloneable
const n: never = structuredClone(Symbol.iterator);
// not cloneable
const p: never = structuredClone(() => 1);
const r = structuredClone({ a: 1, b: 2, c: 3 });

KisaragiEffective avatar Jan 30 '24 09:01 KisaragiEffective

ありがとうございます。cloneできないときに返り値がneverになるのは型安全性の面でまずいと考えており、structuredCloneの呼び出し自体に型エラーが発生するような定義が望ましいと考えています。(いわゆるinvalid typeが欲しくなる……)

structuredClone<T extends StructedClonable>(val: T) みたいな定義になっている必要がありそうです。(実現できるのかちょっとまだ検討していませんが)

uhyo avatar Jan 30 '24 09:01 uhyo

型について再帰的に言及するために、T extends StructuralCloneable<T>のような形になりそうですね。実態としては再帰的にチェックした上でクローンできるならT自身、できないならneverに解決される型関数を書くことになると思います。

KisaragiEffective avatar Jan 30 '24 11:01 KisaragiEffective

// 承前
type AvoidCyclicConstraint<T> = [T] extends [infer R] ? R : never;

declare function x<const T extends StructuredCloneOutput<AvoidCyclicConstraint<T>>>(a: T): StructuredCloneOutput<T>;

x(() => 1);

できたにはできました。以下は動作原理の説明です。

  1. StructuredCloneOutputnever以外に解決される場合→何も起きない
  2. StructuredCloneOutputneverに解決される場合
    1. T extends neverneverに解決される
    2. (arg0: never): neverとなる
    3. neverに属する値は通常のコントロールフローでは生成できないので引数でTS2345

ただ、これをstructuredCloneに適用しようとするとT=neverに解決された場合このオーバーロードが解決の候補から外れて<T = any>(...): Tの方にフォールバックしてしまうようです。

KisaragiEffective avatar Jan 31 '24 00:01 KisaragiEffective

関数として呼び出せるシグネチャのプロパティが残存する問題と、配列及びタプルのreadonly修飾子が消えない問題を修正しました: playground

KisaragiEffective avatar Jan 31 '24 08:01 KisaragiEffective

こちらお待たせしました。この定義を取り入れる方向で試していますが、次のケースで想定通りの結果が出ないようです。自分も見ていますが、修正に協力いただけると助かります 😇

type B = StructuredCloneOutput<{a: Weirdo}>;

uhyo avatar Mar 10 '24 12:03 uhyo

Writable内部のMapped typeで再構成されるとWeirdoの原型が消滅してしまい、HitWeakenEntryにかからなくなるのだと思います。

KisaragiEffective avatar Mar 10 '24 13:03 KisaragiEffective

↑これは若干不正確で、正確には以下のような作用です。

  1. SCO<{a: Weirdo}>を計算するときに
  2. Writeable<Weirdo>の計算が走る (結果をXとする)
  3. Weirdoの原型が失われる
  4. XWeirdoではないので、HitWeakenEntryにかからなくなる
  5. Int16Arrayのプロパティが散らかされて大変なことになる

KisaragiEffective avatar Mar 11 '24 07:03 KisaragiEffective