web-dev-qa-db-ja.com

フィールドを遅延として宣言する

TypeScriptで、フィールドを遅延初期化として宣言するための構文はありますか?

Scalaにあるように、例えば:

lazy val f1 = new Family("Stevens")

フィールド初期化子は、フィールドが最初にアクセスされたときにのみ実行されることを意味します。

16
KarolDepka

TypeScriptで@lazyInitializeを自分では使用できないので、書き換える必要があります。ここに私のデコレータがあります。コピーして使用するだけです。代わりに、プロパティではなくゲッターで@lazyを使用します。

@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
    };
}

テスト

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

    });
});
6
holi-java

私はゲッターを使います:

class Lazy {
  private _f1;

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

}

はい、デコレータでこれに対処することができますが、単純なケースではそれはやり過ぎかもしれません。

12
user663031