Calling JSON.serialize and then JSON.parse on a value is the fastest general-purpose way to deeply clone an object in Javascript. However, the downside is that it only works on values for which x => JSON.parse(JSON.serialize(x)) is the identity function. In other words, this function will only work correctly on strings, numbers, booleans, null, undefined, and arrays and objects built off of these types.
It's common to choose a function name or leave comments warning about this constraint. However, in Typescript we can enforce this constraint at the type level using a recursive type:
// note that the primitives Symbol and BigInt are not serializable
// this is also the same as the JSON type from Typescript's docs, but with the
// addition of undefined as a valid value:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases
type FastClonable =
| string
| number
| boolean
| null
| undefined
| FastClonable[]
| { [x: string]: FastClonable }
const fastClone = <T extends FastClonable>(x: T): T = JSON.parse(JSON.serialize(x))
Now, when passing any value that will not be transparently cloned via this function, Typescript will emit an error.
type FooType = {
bar: {
baz: number
}
}
interface FooInterface {
bar: {
baz: number
}
}
const myFoo = {
bar: {
baz: 123
}
}
// correctly returns a value with type FooType
fastClone(myFoo as FooType)
// Argument of type 'FooInterface' is not assignable to parameter of type 'FastClonable'.
// Type 'FooInterface' is not assignable to type '{ [x: string]: FastClonable; }'.
// Index signature is missing in type 'FooInterface'.
fastClone(myFoo as FooInterface)