Search code examples
typescriptlazy-initialization

Declaring a field as lazy


In TypeScript, is there a syntax for declaring a field as lazily-initialized?

Like there is in Scala, for example:

lazy val f1 = new Family("Stevens")

Meaning that the field initializer would only run when the field is first accessed.


Solution

  • I find it can't using @lazyInitialize in typescript for yourself.so you must rewrite that.here is my decorator,you just to copy and use it.using @lazy on a getter not a property instead.

    @lazy

    const {defineProperty, getPrototypeOf}=Object;
    export default function lazy(target, name, {get:initializer, enumerable, configurable, set:setter}: PropertyDescriptor={}): any {
        const {constructor}=target;
        if (initializer === undefined) {
            throw `@lazy can't be set as a property \`${name}\` on ${constructor.name} class, using a getter instead!`;
        }
        if (setter) {
            throw `@lazy can't be annotated with get ${name}() existing a setter on ${constructor.name} class!`;
        }
    
        function set(that, value) {
            if (value === undefined) {
                value = that;
                that = this;
            }
            defineProperty(that, name, {
                enumerable: enumerable,
                configurable: configurable,
                value: value
            });
            return value;
        }
    
        return {
            get(){
                if (this === target) {
                    return initializer;
                }
                //note:subclass.prototype.foo when foo exists in superclass nor subclass,this will be called
                if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) {
                    return initializer;
                }
                return set(this, initializer.call(this));
            },
            set
        };
    }
    

    Test

    describe("@lazy", () => {
        class Foo {
            @lazy get value() {
                return new String("bar");
            }
    
            @lazy
            get fail(): string {
                throw new Error("never be initialized!");
            }
    
            @lazy get ref() {
                return this;
            }
        }
    
    
        it("initializing once", () => {
            let foo = new Foo();
    
            expect(foo.value).toEqual("bar");
            expect(foo.value).toBe(foo.value);
        });
    
        it("could be set @lazy fields", () => {
            //you must to set object to any
            //because typescript will infer it by static ways
            let foo: any = new Foo();
            foo.value = "foo";
    
            expect(foo.value).toEqual("foo");
        });
    
        it("can't annotated with fields", () => {
            const lazyOnProperty = () => {
                class Bar {
                    @lazy bar: string = "bar";
                }
            };
    
            expect(lazyOnProperty).toThrowError(/@lazy can't be set as a property `bar` on Bar class/);
        });
    
        it("get initializer via prototype", () => {
            expect(typeof Foo.prototype.value).toBe("function");
        });
    
        it("calling initializer will be create an instance at a time", () => {
            let initializer: any = Foo.prototype.value;
    
            expect(initializer.call(this)).toEqual("bar");
            expect(initializer.call(this)).not.toBe(initializer.call(this));
        });
    
        it("ref this correctly", () => {
            let foo = new Foo();
            let ref: any = Foo.prototype.ref;
    
            expect(this).not.toBe(foo);
            expect(foo.ref).toBe(foo);
            expect(ref.call(this)).toBe(this);
        });
    
        it("discard the initializer if set fields with other value", () => {
            let foo: any = new Foo();
            foo.fail = "failed";
    
            expect(foo.fail).toBe("failed");
        });
    
        it("inherit @lazy field correctly", () => {
            class Bar extends Foo {
            }
    
            const assertInitializerTo = it => {
                let initializer: any = Bar.prototype.ref;
                let initializer2: any = Foo.prototype.ref;
                expect(typeof initializer).toBe("function");
                expect(initializer.call(it)).toBe(it);
                expect(initializer2.call(it)).toBe(it);
            };
    
            assertInitializerTo(this);
            let bar = new Bar();
            assertInitializerTo({});
            expect(bar.value).toEqual("bar");
            expect(bar.value).toBe(bar.value);
            expect(bar.ref).toBe(bar);
            assertInitializerTo(this);
        });
    
    
        it("overriding @lazy field to discard super.initializer", () => {
            class Bar extends Foo {
                get fail() {
                    return "error";
                };
            }
    
            let bar = new Bar();
    
            expect(bar.fail).toBe("error");
        });
    
        it("calling super @lazy fields", () => {
            let calls = 0;
            class Bar extends Foo {
                get ref(): any {
                    calls++;
                    //todo:a typescript bug:should be call `super.ref` getter  instead of super.ref() correctly in typescript,but it can't
                    return (<any>super["ref"]).call(this);
                };
            }
    
            let bar = new Bar();
    
            expect(bar.ref).toBe(bar);
            expect(calls).toBe(1);
        });
    
        it("throws errors if @lazy a property with setter", () => {
            const lazyPropertyWithinSetter = () => {
                class Bar{
                    @lazy
                    get bar(){return "bar";}
                    set bar(value){}
                }
            };
    
    
            expect(lazyPropertyWithinSetter).toThrow(/@lazy can't be annotated with get bar\(\) existing a setter on Bar class/);
    
        });
    });