Safe JSON clone

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.

// valid
fastClone(123)
fastClone([123, true, 'foo'])
fastClone({ foo: 123 })

// invalid
fastClone(Symbol('foo'))
fastClone(123n)
fastClone(new Foo())

This also works generically. However, note that this approach does not work when passed an interface, which is due to an intentional Typescript design decision. (Note that this may change in future versions of Typescript.)

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)

Last updated