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