Skip to content
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

Support for structuredClone #1237

Open
cawa-93 opened this issue Jan 1, 2022 · 17 comments
Open

Support for structuredClone #1237

cawa-93 opened this issue Jan 1, 2022 · 17 comments

Comments

@cawa-93
Copy link
Contributor

cawa-93 commented Jan 1, 2022

New DOM api structuredClone should be added.

MDN: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

@saschanaz
Copy link
Contributor

Thanks for the ping. It will be automatically added when it gets multiple implementations.

@yume-chan
Copy link
Contributor

yume-chan commented Mar 2, 2022

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

@saschanaz
Copy link
Contributor

Yeah, #1282. 😬

@HolgerJeromin
Copy link
Contributor

fixed with #1283

@saschanaz
Copy link
Contributor

but it need an override to become generic:

I guess this one still needs to be done

@pragmat1c
Copy link

Any progress here?

@saschanaz
Copy link
Contributor

PRs welcome.

@ehoogeveen-medweb
Copy link

ehoogeveen-medweb commented Jun 21, 2022

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.

@GabrielDelepine
Copy link

GabrielDelepine commented Dec 8, 2022

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?

@GabrielDelepine
Copy link

GabrielDelepine commented Dec 8, 2022

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

@cawa-93
Copy link
Contributor Author

cawa-93 commented Dec 11, 2022

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>
  1. Return type = value type
  2. In the returned type, all fields are writable. But maybe returned type should keep readonly?
  3. 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

Playground

@ehoogeveen-medweb
Copy link

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).

@conorbrandon
Copy link

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?).

@molisani
Copy link

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.

@saschanaz
Copy link
Contributor

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.

@molisani
Copy link

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).

@KisaragiEffective
Copy link

Seems a bit too late to share more robust implementation, how about snippet in uhyo/better-typescript-lib#37 (comment) ?
It eliminates functions and symbols, inheritance, therefore more robust about both argument and return type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants