Home > front end >  How to call return from a constructor without marking properties as undefined?
How to call return from a constructor without marking properties as undefined?

Time:02-01

I use a file of exported constant as preprocessor directives. Now I would like to completely strip the body of a constructor if this directive is set. For example:

const MY_PREPROCESSOR_DIRECTIVE = true;

class Foo {
    someProp: string;
    constructor() {
        if (MY_PREPROCESSOR_DIRECTIVE) return;

        this.someProp = "string";
    }
}

This works nicely, I'm using closure compiler for building, which strips the body and the whole thing is turned into only just:

class Foo {}

In TypeScript this causes errors though, because someProp and otherProp are "not definitely assigned in the constructor". And in JavaScript files, I don't get errors in the constructor, but all properties are getting set to string | undefined:

const MY_PREPROCESSOR_DIRECTIVE = false;

class Foo {
    constructor() {
        if (MY_PREPROCESSOR_DIRECTIVE) return;

        this.someProp = "string";
    }

    myMethod() {
        if (MY_PREPROCESSOR_DIRECTIVE) return;

        this.someProp.substring(0);
//      ^^^^^^^^^^^^^------ Object is possibly 'undefined'.
    }
}

(js playground | ts playground)

Is there a way to let TypeScript know the directive will always be false so that it doesn't mark this.someProp as string | undefined? I tried to mark the return statement of the constructor as unreachable code, but I'm not sure how to achieve this.

CodePudding user response:

Since TS know nothing bout runtime MY_PREPROCESSOR_DIRECTIVE` I think it worth using strategy pattern.

SO, you have two variants of Foo class. One with MY_PREPROCESSOR_DIRECTIVE = true and another one with MY_PREPROCESSOR_DIRECTIVE = false.

const MY_PREPROCESSOR_DIRECTIVE = false;

interface Foo {
    someProp?: string;
    myMethod: () => void
}

class WithDirective implements Foo {
    someProp: string;

    constructor() {
        this.someProp = "string";
    }
    myMethod() {
        this.someProp.substring(0)
    }
}

class WithoutDirective implements Foo {
    someProp: undefined
    constructor() { }
    myMethod() { }
}

const Foo = MY_PREPROCESSOR_DIRECTIVE ? WithDirective : WithoutDirective

Playground

If you don't like the fact that there will be always 1 unused class in a build, you can use class expression:

const MY_PREPROCESSOR_DIRECTIVE = false;

interface Foo {
    someProp?: string;
    myMethod: () => void
}

const Foo =
    MY_PREPROCESSOR_DIRECTIVE
        ? class implements Foo {
            someProp: string;

            constructor() {
                this.someProp = "string";
            }
            myMethod() {
                this.someProp.substring(0)
            }
        }
        : class implements Foo {
            someProp: undefined
            constructor() { }
            myMethod() { }
        }


Personally, I think first example is more readable and easier to maintain. Also, I don't think that it will have a significant impact on your bundle size.

CodePudding user response:

It seems one way to work around this issue, in JavaScript anyway, is to explicitly set the type using JSDoc:

constructor() {
    if (MY_PREPROCESSOR_DIRECTIVE) return;

    /** @type {string} */
    this.someProp = "string";
}

(playground)

I'm not sure if there's a similar way to do this with TypeScript, so this might very well be unintentional behaviour.

  •  Tags:  
  • Related