TypeScript-DOM-lib-generator
TypeScript-DOM-lib-generator copied to clipboard
Support for `structuredClone`
New DOM api structuredClone should be added.
MDN: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
Thanks for the ping. It will be automatically added when it gets multiple implementations.
Note: @mdn/[email protected] now has enough browser implementations for structuredClone, but it need an override to become generic:
declare function structuredClone(value: any, options?: StructuredSerializeOptions): any;
And 4.1.8 breaks XMLHttpRequestEventTarget
Yeah, #1282. 😬
fixed with #1283
but it need an override to become generic:
I guess this one still needs to be done
Any progress here?
PRs welcome.
FWIW, one issue I ran into with my own generic version is that if the source object is (deeply) readonly (I had a complex template object defined with as const to get good type information), the result of the structured clone is also considered (deeply) readonly.
In my case, the whole point was to clone the template object a few times and then write to the clones, so I had to add a hacky DeepWritable<T> to make it work. I don't know whether it always makes sense for readonly to be stripped as the result of cloning, but simply specifying that the output type match the input type might not be ideal either.
Any chance we could get a typed return value in the future?
Something like declare function structuredClone(value: T, options?: StructuredSerializeOptions): T; ?
I know that structuredClone is not returning a strictly identical object (functions are silently removed for instance).
Maybe that's the reason to use : any here?
My current "workaround" (let me know what you think)
type MyType = {
ok?: boolean
}
const foo: MyType = {}
const bar = structuredClone(foo) as typeof foo
/*
Why use "typeof foo" instead of "MyType" above?
- To be sure to pick the right type
- To define the type only in 1 place (at the definition of "foo")
- That way, changing the type of "foo" in the future will automatically apply to "bar".
*/
bar.ok = true // OK
bar.something = 'else' // Typescript error, because bar belongs to MyType
This type declaration works for me:
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>
- Return type = value type
- In the returned type, all fields are writable. But maybe returned type should keep
readonly? - Will throw a type error if the passed object at any depth contains data that cannot be cloned (function or symbol). This is most important, in my opinion.
const value = {foo: 1} as const
structuredClone(value) // { foo: 1 } Note, foo now writable
const value = {foo: 1, fn() {}} as const
structuredClone(value) // expect type error because function can't be cloned and it will throw DOMException in runtime
That one failed for me on types containing arrays (or maybe just tuples). I managed to get my overly complex type to work using this:
type Cloneable<T> = T extends Function | Symbol
? never
: T extends readonly [infer e0]
? readonly [Cloneable<e0>]
: T extends [infer e1]
? [Cloneable<e1>]
: T extends readonly [infer e2, ...infer A2]
? readonly [Cloneable<e2>, ...Cloneable<[...A2]>]
: T extends [infer e3, ...infer A3]
? [Cloneable<e3>, ...Cloneable<[...A3]>]
: T extends readonly (infer e4)[]
? readonly Cloneable<e4>[]
: T extends (infer e5)[]
? Cloneable<e5>[]
: T extends object
? { [k in keyof T]: Cloneable<T[k]> }
: T;
That one does not strip readonly however (which is fine for me, but maybe not in general).
Props to both attempts, unfortunately both fail for me with branded types, which I understand is somewhat of a second class citizen in TS, but are used in the official nominal typing example in the playground. Note I also tried the unique symbol method of branding. Chucking in a T extends Primitive ? T : ... where type Primitive = string | number | boolean | null | undefined | symbol; fixes it (I think?).
I'm slightly worried that the recently merged generic type signature for structuredClone is a bit too permissive. While the PR mentions the lack of support for the error case where a provided object causes the function to throw an error, it does not address properties that are not preserved (see third bullet point). I think this is an even more important case as it is one that will not surface at the structuredClone callsite, but instead elsewhere in the code where a property is unexpectedly undefined.
I merged it because it's certainly better than any, but yes, it could be confusing as now the IDE will suggest fields that can't be cloned. Not sure there's a better way, though.
Well anything is better than any but I do understand the usability of having an entirely permissive signature for this function. I manage types for an internal codebase that recently added support for structuredClone and our definition was (unknown) => unknown with the assertion being that if you wanted to use it, you would need to manually cast the cloned result to whatever type you wanted. From a type perspective the type cast would be equivalent to the "identity type parameter" signature used above, but with the added benefit of having an explicit cast that raises the appropriate yellow flags (i.e. lint warnings).
Seems a bit too late to share more robust implementation, how about snippet in https://github.com/uhyo/better-typescript-lib/issues/37#issuecomment-1916384592 ? It eliminates functions and symbols, inheritance, therefore more robust about both argument and return type.