DEV Community

Forrest Cahoon
Forrest Cahoon

Posted on

Helping Typescript make correct type inferences

One of the things that makes Typescript useful is its ability to correctly infer variable types. The most common inference I rely on is when I have a variable that might be undefined. I handle the undefined case, and Typescript knows that, for the remainder of the method, that variable must be defined.

Usually.

I was having trouble with this sort of inference not working sometimes, and I finally tracked down when it occurs and how I can work around it.

First, let's talk about what does work.

This code compiles without any errors:

const example1 = (mayBeDefined?: string) => {
    if (mayBeDefined === undefined) {
        console.log("mayBeDefined is undefined");
        return;
    }
    const definitelyDefined: string = mayBeDefined;
    console.log(`mayBeDefined is set to ${definitelyDefined}`);
};
Enter fullscreen mode Exit fullscreen mode

(Note that we're explicitly casting definitelyDefined here to force an error when Typescript doesn't infer that mayBeDefined can no longer be undefined.)

When we return from our function in the case where mayBeDefined is undefined, Typescript's inference works correctly.

 

Often an undefined value, while allowed, is an exceptional condition, so we want to throw an error. How does that work?

const example2 = (mayBeDefined?: string) => {
    if (mayBeDefined === undefined) {
        throw new Error("mayBeDefined is undefined");
    }
    const definitelyDefined: string = mayBeDefined;
    console.log(`mayBeDefined is set to ${definitelyDefined}`);
};
Enter fullscreen mode Exit fullscreen mode

Once again, Typescript infers correctly that mayBeDefined must be defined after the if block. Yay!

 

Sometimes, though, we might want to do something else (like logging) whenever we throw an error, in which case it makes sense to put that into another function. Let's look at that scenario.

const logAndThrow = (msg: string) => {
    console.log(msg);
    throw new Error(msg);
}

const example3 = (mayBeDefined?: string) => {
    if (mayBeDefined === undefined) {
        logAndThrow("mayBeDefined is undefined");
    }
    const definitelyDefined: string = mayBeDefined;
    console.log(`mayBeDefined is set to ${definitelyDefined}`);
};
Enter fullscreen mode Exit fullscreen mode

This is a bit much for Typescript to follow. This time we get the error

Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.
Enter fullscreen mode Exit fullscreen mode

Shoot. WE know that mayBeDefined must be defined after the if block. We could just declare

const definitelyDefined = mayBeDefined as string;
Enter fullscreen mode Exit fullscreen mode

and be done with it. After all, that's what the as construct is for: second-guessing Typescript when it can't make correct inferences. But that's trusting us to make correct type inferences, which is also risky.

 

It would be better if there was some way to give Typescript's inference engine a little nudge to make it see what's going on here. Let's try this:

const logAndThrow = (msg: string) => {
    console.log(msg);
    throw new Error(msg);
}

const example4 = (mayBeDefined?: string) => {
    if (mayBeDefined === undefined) {
        logAndThrow("mayBeDefined is undefined");
        return; // unreachable but helps typescript inferences
    }
    const definitelyDefined: string = mayBeDefined;
    console.log(`mayBeDefined is set to ${definitelyDefined}`);
};
Enter fullscreen mode Exit fullscreen mode

Typescript is happy again! the return statement in the if block is unreachable code, but Typescript doesn't know that. It's non-intuitive to add that line (and I recommend commenting it to let your fellow devs know why it's there).

I don't know how common this issue is in the larger Typescript community, but I use it a lot, and I'm really glad I was able to find this solution.

I hope some of you also find it helpful.

Top comments (0)