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.
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.
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.
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/);
});
});
return super.ref
works for me, which is correct here. –
Kandace [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 set:setter||set
, but I just disable this feature by throw an exception –
Flogging 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.
return this._f1
. But I like your solution for the simplicity. –
Kandace this._f1
otherwise –
Carmine ??
instead of ||
: return this._f1 ?? this._f1 = expensiveInitializationForF1();
. This avoid running the initialization if _f1
is initialized but has a falsy value. –
Karole #f1
instead of private _f1
for true privacy. –
Timekeeper ||
with ??
to better handle certain values like zero. –
Pipsqueak 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.
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/);
});
});
return super.ref
works for me, which is correct here. –
Kandace [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 set:setter||set
, but I just disable this feature by throw an exception –
Flogging 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.
??=
I've literally never seen that in my life –
Lyckman 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;
© 2022 - 2024 — McMap. All rights reserved.
@lazy val f1 = new Family("Stevens").
– Flogging