DEV Community

Ivan Montiel
Ivan Montiel

Posted on • Originally published at idmontie.github.io

Using Object.create to clone

In “Javascript: The Definitive Guide” there is an example that uses Object.inherit to inherit the prototype change. JavaScript defines a method Object.create that creates a new object using the given argument as the prototype of that object. Translating the examples from The Definitive Guide to Typescript, it looks like:

function inherit<T extends object>(obj: T): T {
    return Object.create(obj);
}

class MyObject {
    public a: number;

    constructor(a: number) {
        this.a = a;
    }
}
const myObject = new MyObject(123);
const myClone = inherit(myObject);
Enter fullscreen mode Exit fullscreen mode

Playground Link

However, we lied. Notice that in the above code, the myClone object was created without a being initialized.

console.log(myObject); // { a: 123 }
console.log(myClone); // {}
console.log(myClone instanceof MyObject); // true
Enter fullscreen mode Exit fullscreen mode

This creates what is known as a type-hole: the Typescript compiler will not report any bugs when we try to use myClone.a. That’s because we used Object.create which returns the any type. In the above example, if we tried to use myClone.a in a case where we expected a number, but got undefined, we can end up with runtime bugs that should have been caught by the compiler.

We can make the typings a little more clear by doing the following:

function inherit<T extends object>(obj: T): Partial<T> {
    return Object.create(obj);
}
Enter fullscreen mode Exit fullscreen mode

Now Typescript will report that the value of myClone.a might be undefined.

Let’s improve this a bit more and create an object that inherits and freezes the data in the given object:

function inheritAndFreeze<T extends object>(obj: T, values: Partial<T>): Readonly<Partial<T>> {
    const properties: PropertyDescriptorMap = {};

    Object.keys(obj).forEach((unsafeKey) => {
        const key = unsafeKey as keyof T;
        properties[key] = {
            value: values[key] ?? undefined,
            writable: false,
                        configurable: false,
        };
    })

    return Object.create(obj, properties);
}

class MyObject {
    public a: number;

    constructor(a: number) {
        this.a = a;
    }
}

const myObject = new MyObject(123);
const myClone = inheritAndFreeze(myObject, { a: 123 });

console.log(myClone); // { a: 123 }
myClone.a = 999; // Would throw "Cannot assign to read only property 'a' of object '#<MyObject>' "
Enter fullscreen mode Exit fullscreen mode

Playground Link

If we didn’t mark the return value as Readonly<Partial<T>>, and instead just had Partial<T> we would once again have a type-hole and myClone.a = 999; would be allowed by Typescript, but would throw an exception at runtime because property a is read only as a runtime constraint.

The Takeaway

Let’s step back and really look at what inherit was doing. All it gave us was a template object, and if you read on MDN about Inheritance and prototype chaining:

You may also see some legacy code using [Object.create()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) to build the inheritance chain. However, because this reassigns the prototype property and removes the [constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor) property, it can be more error-prone, while performance gains may not be apparent if the constructors haven't created any instances yet.

The key here is to be wary of using utility functions built into the language that return the any type. Be careful what the Typescript constraints are and make sure you don’t have any type-holes when you are using these generic functions.

Top comments (0)