0

I have a class that employs lazy initialisation.

class LazyWorker {
    private state: string | undefined;

    lazyInit() {
        if (this.state === undefined) {
            //A lot of statements here
            //That are guaranteed to initialise the this.state property
            this.state = 'aaa'

            //I don't want to repeat those statements...
            //DRY principle, right? 
        }
    }

    doSomething() {
        this.lazyInit();

        this.state.startsWith('doSomething');
    }

    doSomethingElse() {
        this.lazyInit();

        this.state.endsWith('doSomethingElse');
    }
}

Unfortunately, in the doSomething methods, the compiler complains that this.state might be undefined.

I can hack around compiler complaining by defining the property the following way, but it is inelegant and might give the false impression that the property is never undefined.

class LazyWorker {
    private state: string = undefined as any as string;

Any elegant way around it?

Grogi
  • 2,099
  • 17
  • 13

2 Answers2

0

If state is generally not undefined you might consider removing the undefined from the type and using a definite assignment assertion:

class LazyWorker {
    private state!: string;
    // ...
}

Another option is to use a not null assertion every time you access state:

class LazyWorker {
    private state: string | undefined;

    initializeState() {
        //A lot of statements here
        //That are guaranteed to initialise the this.state property
        this.state = 'aaa'

        //I don't want to repeat those statements...
        //DRY principle, right? 
    }

    doSomething() {
        this.initializeState();

        this.state!.startsWith('doSomething');
    }

    doSomethingElse() {
        this.initializeState();

        this.state!.endsWith('doSomethingElse');
    }
}

Both of these options are not type save and can cause run-time errors.

Typescript does not support custom assertion functions, it only supports type guards, which require an if to work as expected, but only for public properties. We can get this to work, but I'm not sure if it's not overkill:

class LazyWorker {
    state: string | undefined;

    initializeState(): this is this & { state: string } {
        //A lot of statements here
        //That are guaranteed to initialise the this.state property
        this.state = 'aaa'

        //I don't want to repeat those statements...
        //DRY principle, right? 
        return true;
    }

    doSomething() {
        if(!this.initializeState()) return;

        this.state.startsWith('doSomething');
    }

    doSomethingElse() {
        if(!this.initializeState()) return;

        this.state.endsWith('doSomethingElse');
    }
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
0
  1. use non-null assertion (exclamation mark)
this.state!.startsWith('doSomething');
  1. If possible, find a why to initialize state in the constructor
  private state: string;

  constructor() {
    this.state = 'aaa'
  }

Typescript is still new, and still require these kind of tweak to make it work properly.

For this matter, I think your suggested method is good as well, and in any case - don’t let TypeScript slow you down

yuval.bl
  • 4,890
  • 3
  • 17
  • 31
  • Initialising the state in the constructor is against lazy-initialisation. It's done on purpose like that. – Grogi Jul 10 '19 at 13:06