Declaring a field as lazy
Asked Answered
P

4

30

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.

Pipsqueak answered 16/3, 2017 at 21:59 Comment(8)
you may be need to write a decorator to support that.like:@lazy val f1 = new Family("Stevens").Flogging
Interesting, thanks. Upvoted. Anything like that ready-made somewhere? E.g. in Angular 2?Pipsqueak
see https://github.com/jayphelps/core-decorators.js#lazyinitializeFlogging
@MaciejBukowski inspired by you,and I'm change my decorator that could be inherited correctly.I'll update my answer,please help me to see that every test is right in the test section?Flogging
@Flogging I deleted my comment because I found myself wrong ;) I tested the decorators in TS with your first version and TS is replacing the original method in the prototype with the decorated one, once the program starts, so there aren't any time or memory run-time issues. But I'll run your tests to be sure :)Kandace
my test write with ts-jest,I think that tests would be run any xunit.Flogging
good to know because it's incompatible with chai and expect packages and I changed few lines to make it compatible with the expect. Why every unit test framework has to redefine assert functions?Kandace
see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Aiello
F
13

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/);

    });
});
Flogging answered 16/3, 2017 at 22:57 Comment(5)
Hmm. I'm using [email protected] and only return super.ref works for me, which is correct here.Kandace
because you set compilerOptions target to es6,that I have tested,in es5 it is can't be called super getters.Flogging
Yep. You're right. But if I choose es5 target none of the them works for me :D I'm getting [ts] Only public and protected methods of the base class are accessible via the 'super' keyword. and [ts] Cannot invoke an expression whose type lacks a call signature. Type 'Bar' has no compatible call signatures.Kandace
Yes,I found some new situations,I'll updated my answer again.Flogging
may be I think that is wrong,I think @lazy on a getter that existing a setter will cause the setter with invalid behavior.@lazy can do it by set:setter||set, but I just disable this feature by throw an exceptionFlogging
L
36

I would use a getter:

class Lazy {
  private _f1;

  get f1() {
    return this._f1 || (this._f1 = expensiveInitializationForF1());
  }

}

Yes, you could address this with a decorator, but that might be overkill for simple cases.

Leinster answered 17/3, 2017 at 2:28 Comment(5)
There's a small typo - should be return this._f1. But I like your solution for the simplicity.Kandace
You have to put the assignment in parentheses, typescript complains about a missing semicolon after the second this._f1 otherwiseCarmine
Today, you should probably go for ?? instead of ||: return this._f1 ?? this._f1 = expensiveInitializationForF1();. This avoid running the initialization if _f1 is initialized but has a falsy value.Karole
With new private class fields recently shipped in ECMAScript, you can use #f1 instead of private _f1 for true privacy.Timekeeper
These days one should replace || with ?? to better handle certain values like zero.Pipsqueak
F
13

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/);

    });
});
Flogging answered 16/3, 2017 at 22:57 Comment(5)
Hmm. I'm using [email protected] and only return super.ref works for me, which is correct here.Kandace
because you set compilerOptions target to es6,that I have tested,in es5 it is can't be called super getters.Flogging
Yep. You're right. But if I choose es5 target none of the them works for me :D I'm getting [ts] Only public and protected methods of the base class are accessible via the 'super' keyword. and [ts] Cannot invoke an expression whose type lacks a call signature. Type 'Bar' has no compatible call signatures.Kandace
Yes,I found some new situations,I'll updated my answer again.Flogging
may be I think that is wrong,I think @lazy on a getter that existing a setter will cause the setter with invalid behavior.@lazy can do it by set:setter||set, but I just disable this feature by throw an exceptionFlogging
I
7

Modern version, classes:

class Lazy<T> {
    #value?: T;
    constructor(public valueFactory: () => T) {}
    public get value(): T {
        return (this.#value ??= this.valueFactory());
    }
}

Modern version, inline:

let value;
// …
use_by_ref(value ??= lazy_init());

Potential future version, with proxies, that doesn't currently work because returning a different value from a constructor is usually considered as undefined behavior.

class Lazy<T> {
  constructor(private init: { [K in keyof T]: () => T[K] }) {
    let obj = Object.fromEntries(Object.keys(init).map(k => [k, undefined])) as unknown as { [K in keyof T]: undefined | T[K] };
    Object.seal(obj);
    return new Proxy(obj, this);
  }
  get<K extends keyof T>(t: T, k: K): T[K] {
    return t[k] ??= this.init[k];
  }
}

Can probably simplify more.

Infecund answered 19/8, 2020 at 15:54 Comment(3)
Sorry I have to ask, what's ??= I've literally never seen that in my lifeLyckman
@beqa, it is a nullish coaleascing short-circuiting operator. Check the last paragraph of this article to know more about it!Kerns
oh right, it's just like += or -= ... I never thought to use that on ?? though. Interesting. I learned something today.Ginzburg
A
1

I'm using something like this:

export interface ILazyInitializer<T> {(): T}

export class Lazy<T> {
private instance: T | null = null;
private initializer: ILazyInitializer<T>;

 constructor(initializer: ILazyInitializer<T>) {
     this.initializer = initializer;
 }

 public get value(): T {
     if (this.instance == null) {
         this.instance = this.initializer();
     }

     return this.instance;
 }
}



let myObject: Lazy<MyObject>;
myObject = new Lazy(() => <MyObject>new MyObject("value1", "value2"));
const someString = myObject.value.getProp;
Anuria answered 4/8, 2021 at 15:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.