Skip to content Skip to sidebar Skip to footer

Can Typescript Infer That An Argument Has Been Validated?

I'm still learning Typescript and Javascript so please excuse me if I am missing something. The problem is as follows: Currently VSCode does not infer that I throw an error if emai

Solution 1:

You can use a type guard. The type guard asserts the type of a particular variable like this:

exportdefaultclassValidate {
  publicstaticvalidateEmail(email?: string) {
    const emailRegex = newRegExp(
      '^(([^<>()[]\\.,;:s@"]+(.[^<>()[]\\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$'
    );

    if (!Validate.defined(email)) {
      thrownewSyntaxError('The text recieved was not defined.');
    }

    if (!emailRegex.test(email)) {
      thrownewSyntaxError('The email entered is not valid');
    }
  }

  publicstaticdefined(text?: string): text is string {
    return !!text;
  }

  publicstaticnotEmpty(text: string) {
    if (text.length < 1) {
      thrownewSyntaxError('The text entered is empty.');
    }
  }
}

The downside is that you still need an if statement and the throw expression is outside of the function.

Or you can do something like this, which avoids those problems, but requires you to reassign email to the result of the function.

exportdefaultclassValidate {
  publicstaticvalidateEmail(email?: string) {
    const emailRegex = newRegExp(
      '^(([^<>()[]\\.,;:s@"]+(.[^<>()[]\\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$'
    );

    email = Validate.defined(email);

    if (!emailRegex.test(email)) {
      thrownewSyntaxError('The email entered is not valid');
    }
  }

  publicstaticdefined(text?: string): string {
    if (!text) {
      thrownewSyntaxError('The text recieved was not defined.');
    }
    return text;
  }

  publicstaticnotEmpty(text: string) {
    if (text.length < 1) {
      thrownewSyntaxError('The text entered is empty.');
    }
  }
}

Solution 2:

The compiler can sometimes recognize that a variable is of a type narrower than its annotated or inferred type at certain points in the code, by analyzing the control flow. In the following code block, the compiler understands that email cannot be undefined if the flow of control reaches the this.notEmpty() call, and thus there is no error:

if (!email) {
    thrownewSyntaxError("The text received was not defined.");
}

this.notEmpty(email); // okay

As you've discovered, though, simply refactoring the code to make the check happen in a different function does not work. The compiler generally does not follow control flow into functions and methods when performing control flow analysis. This is a tradeoff (see Microsoft/TypeScript#9998 in GitHub for more info): it's not feasible for the compiler to simulate the program running on all possible inputs, by analyzing the possible control flow paths through all possible function calls, so it has to use a heuristic somewhere; in this case, the heuristic is generally "assume function calls have no impact on variable types". Therefore the call to this.defined(email) has no impact on the type of email as seen by the compiler, and thus it complains about this.notEmpty(email).


Luckily, TypeScript 3.7 introduced "assertion functions"; you can give a function a special return type that tells the compiler that a variable passed to the function will be narrowed by the function, and it will use this as part of its control flow analysis. While the compiler doesn't infer such function signatures itself, at least now you can manually annotate that defined() asserts something about its argument:

static defined(text?: string): asserts text {
    if (!text) {
        throw new SyntaxError("The text recieved was not defined.");
    }
}

The return type of defined() is asserts text, which means that text will be verified as truthy if defined() returns. And this fixes your original example:

this.defined(email);
this.notEmpty(email); // okay now!

Looks good. Okay, hope that helps; good luck!

Playground link to code

Solution 3:

The reason why your code is not compiling is because notEmpty needs to accept a nullable string. That is because at the call site you are passing a nullable string as an argument.The following code fixes the problems with your code.

exportdefaultclassValidate {
    staticvalidateEmail(email?: string) {
        // TODO: Test this regex properly.const emailRegex = newRegExp('^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')

        this.defined(email);
        this.notEmpty(email);

        if (email && !emailRegex.test(email)) {
            thrownewSyntaxError("The email entered is not valid");
        }
    }

    staticdefined(text?: string) {
        if (!text) {
            thrownewSyntaxError("The text recieved was not defined.");
        }
    }

    staticnotEmpty(text?: string) {
        if (text && text.length < 1) {
            thrownewSyntaxError("The text entered is empty.");
        }
    }
}

Post a Comment for "Can Typescript Infer That An Argument Has Been Validated?"