lukeed/klona

Class instances cloned using `klona/full` do not run field initializers

gebsh opened this issue · 0 comments

gebsh commented

When cloning instances of classes containing instance fields, their initializers are not run:

import { klona as klonaFull } from 'klona/full';

class Foo {
    foo = ( console.log( 'initializing' ), 0 );
}

const foo1 = new Foo();
const foo2 = klonaFull( foo1 );

The preceding example logs initializing only once, even though I'd expect the log to be printed twice.

This bug is not present in klona/lite and klona, most likely due to the fact that they have a branch detecting constructor functions:

import { klona as klonaLite } from 'klona/lite';
import { klona } from 'klona';

class Foo {
    foo = ( console.log( 'initializing' ), 0 );
}

const foo1 = new Foo();
const foo2 = klonaLite( foo1 );
const foo3 = klona( foo1 );

Here, initializing is correctly logged three times.

This behavior has some implications. For example, TypeScript 5.0 supports the newest decorators proposal, and it transpiles such decorators to similar code. The following example:

function log( target: unknown, ctx: ClassFieldDecoratorContext ): void {
    ctx.addInitializer( () => {
        console.log( 'initializing' );
    } );
}

class Foo {
    @log
    foo = 0;
}

is transpiled by the TypeScript compiler as follows:

function log(target, ctx) {
    ctx.addInitializer(() => {
        console.log("initializing");
    });
}
let Foo = (() => {
    let _instanceExtraInitializers = [];
    let _foo_decorators;
    let _foo_initializers = [];
    return class Foo {
        static {
            _foo_decorators = [log];
            __esDecorate(
                null,
                null,
                _foo_decorators,
                {
                    kind: "field",
                    name: "foo",
                    static: false,
                    private: false,
                    access: {
                        has: (obj) => "foo" in obj,
                        get: (obj) => obj.foo,
                        set: (obj, value) => {
                            obj.foo = value;
                        },
                    },
                },
                _foo_initializers,
                _instanceExtraInitializers
            );
        }
        foo =
            (__runInitializers(this, _instanceExtraInitializers),
            __runInitializers(this, _foo_initializers, 0));
    };
})();

Note that some helpers (such as __esDecorate) were omitted for brevity.

When cloning objects that use decorators using klona/full, initializers added via ctx.addInitializer() are not run.