Helping Typescript make correct type inferences
Tags: typescript
Published: 2023-02-11 21:06:18

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:
```typescript
const example1 = (mayBeDefined?: string) => {
    if (mayBeDefined === undefined) {
        console.log("mayBeDefined is undefined");
        return;
    }
    const definitelyDefined: string = mayBeDefined;
    console.log(`mayBeDefined is set to ${definitelyDefined}`);
};
```
(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.

<p>&nbsp;</p>

Often an undefined value, while allowed, is an exceptional condition, so we want to throw an error. How does that work?
```typescript
const example2 = (mayBeDefined?: string) => {
    if (mayBeDefined === undefined) {
        throw new Error("mayBeDefined is undefined");
    }
    const definitelyDefined: string = mayBeDefined;
    console.log(`mayBeDefined is set to ${definitelyDefined}`);
};
```
Once again, Typescript infers correctly that `mayBeDefined` must be defined after the if block. Yay!

<p>&nbsp;</p>

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.
```typescript
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}`);
};
```
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'.
```
Shoot. WE know that `mayBeDefined` must be defined after the if block. We could just declare
```typescript
const definitelyDefined = mayBeDefined as string;
```
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.

<p>&nbsp;</p>

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:
```typescript
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}`);
};
```
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.