better-typescript-lib
better-typescript-lib copied to clipboard
structuredCloneについて
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>;
脳筋な実装をしたのは~~それしか知らなかったから~~と型名がホバーした時にわかりやすくなるからという利点を選択したからですが、メンテナンス性には欠けるかもしれません。
提案ありがとうございます。structuredCloneの型をより安全にするのはよいアイデアだと思いますが、 さすがにこの実装だとパワーがありすぎるのでどんな実装ができそうか検討します🤔
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]> }
のようにすることもできそうですが、これもまたインデントが項目数の二乗に比例してしまいそうです。
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>
ということであれやこれややって、インデントの二乗に比例しないようなコードを得ることができたと思います。
Map
とSet
については型引数があるのでWeaken
に入れることが難しそうです。
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 });
ありがとうございます。cloneできないときに返り値がneverになるのは型安全性の面でまずいと考えており、structuredCloneの呼び出し自体に型エラーが発生するような定義が望ましいと考えています。(いわゆるinvalid typeが欲しくなる……)
structuredClone<T extends StructedClonable>(val: T)
みたいな定義になっている必要がありそうです。(実現できるのかちょっとまだ検討していませんが)
型について再帰的に言及するために、T extends StructuralCloneable<T>
のような形になりそうですね。実態としては再帰的にチェックした上でクローンできるならT自身、できないならneverに解決される型関数を書くことになると思います。
// 承前
type AvoidCyclicConstraint<T> = [T] extends [infer R] ? R : never;
declare function x<const T extends StructuredCloneOutput<AvoidCyclicConstraint<T>>>(a: T): StructuredCloneOutput<T>;
x(() => 1);
できたにはできました。以下は動作原理の説明です。
-
StructuredCloneOutput
がnever
以外に解決される場合→何も起きない -
StructuredCloneOutput
がnever
に解決される場合-
T extends never
がnever
に解決される -
(arg0: never): never
となる -
never
に属する値は通常のコントロールフローでは生成できないので引数でTS2345
-
ただ、これをstructuredClone
に適用しようとするとT=never
に解決された場合このオーバーロードが解決の候補から外れて<T = any>(...): T
の方にフォールバックしてしまうようです。
関数として呼び出せるシグネチャのプロパティが残存する問題と、配列及びタプルのreadonly修飾子が消えない問題を修正しました: playground。
こちらお待たせしました。この定義を取り入れる方向で試していますが、次のケースで想定通りの結果が出ないようです。自分も見ていますが、修正に協力いただけると助かります 😇
type B = StructuredCloneOutput<{a: Weirdo}>;
Writable
内部のMapped typeで再構成されるとWeirdo
の原型が消滅してしまい、HitWeakenEntry
にかからなくなるのだと思います。
↑これは若干不正確で、正確には以下のような作用です。
-
SCO<{a: Weirdo}>
を計算するときに -
Writeable<Weirdo>
の計算が走る (結果をX
とする) -
Weirdo
の原型が失われる -
X
はWeirdo
ではないので、HitWeakenEntry
にかからなくなる -
Int16Array
のプロパティが散らかされて大変なことになる