Skip to content

structuredCloneについて #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
KisaragiEffective opened this issue Jan 28, 2024 · 12 comments · Fixed by #38
Closed

structuredCloneについて #37

KisaragiEffective opened this issue Jan 28, 2024 · 12 comments · Fixed by #38
Labels
enhancement New feature or request

Comments

@KisaragiEffective
Copy link
Contributor

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
Copy link
Contributor Author

KisaragiEffective commented Jan 28, 2024

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

@uhyo uhyo added the enhancement New feature or request label Jan 29, 2024
@uhyo
Copy link
Owner

uhyo commented Jan 30, 2024

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

@KisaragiEffective
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

KisaragiEffective commented Jan 30, 2024

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 });

@uhyo
Copy link
Owner

uhyo commented Jan 30, 2024

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

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

@KisaragiEffective
Copy link
Contributor Author

KisaragiEffective commented Jan 30, 2024

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

@KisaragiEffective
Copy link
Contributor Author

KisaragiEffective commented Jan 31, 2024

// 承前
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
Copy link
Contributor Author

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

@uhyo
Copy link
Owner

uhyo commented Mar 10, 2024

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

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

@KisaragiEffective
Copy link
Contributor Author

KisaragiEffective commented Mar 10, 2024

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

@KisaragiEffective
Copy link
Contributor Author

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

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

@uhyo uhyo closed this as completed in #38 Mar 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants