TypeScript - type guarding against null

TL;DR: you are not crazy, but you're expecting more from the compiler than it can deliver. Use a type assertion and move on.


TypeScript doesn't tend to determine all the possible implications of a type guard check. One of the few places where checking an object's property narrows the type of the object itself is when the object is a discriminated union and the property you are checking is a discriminant property. In your case, par is not itself even a union type, let alone a discriminated union. So when you check par.b the compiler does narrow par.b but does not propagate that narrowing upward into a narrowing of par itself.

It could do this, but the problem is that such computation can easily get expensive for the compiler, as described in this comment by one of the language architects:

It seems that for every reference to x in a control flow graph we would now have to examine every type guard that has x as a base name in a dotted name. In other words, in order to know the type of x we'd have to look at all type guards for properties of x. That has the potential to generate a lot of work.

If the compiler were as clever as a human being, it could possibly perform these extra checks only when they are likely to be useful. Or maybe a clever human being could write up some heuristics that would be good enough for this use case; but I suppose in practice this hasn't been high on anyone's priority list to get into the language. I haven't found an open issue in GitHub that suggests this, so if you feel strongly about it you might want to file one. But I don't know how well received it would be.

In the absence of a cleverer compiler, there are workarounds:


The simplest workaround is to accept that you are smarter than the compiler and use a type assertion to tell it that you are sure what you are doing is safe and that it shouldn't worry too much about verifying it. Type assertions are a bit dangerous in general, since if you use one and are wrong about your assertion, then you've just lied to the compiler and any runtime issues that arise from this are your fault. But in this case, we can be pretty confident:

function aAssert(par: {
  a: string;
  b: null | string;
}): { a: string; b: string } | undefined {

  if (par.b === null) {
    return;
  }

  return par as { a: string; b: string }; // I'm smarter than the compiler 🤓
}

This is probably the way to go here, because it keeps your code essentially the same and the assertion is pretty mild.


Another possible workaround is to use a user-defined type guard function which narrows par itself. It's a bit tricky because type guard functions that don't act on union types don't narrow in the "else" branch... probably because the language lacks negated types. That is, if you have a type guard function guard(x: A): x is A & B, and call if (guard(x)) { /*then branch*/ } else { /*else branch*/ }, x will be narrowed to A & B inside the "then" branch, but will just be A in the "else" branch. There is no A & not B type to use. The closest you can get is to do if (!guard(x)) {} else {}, but that just switches which branch gets narrowed.

So we can do this:

function propNotNull<T, K extends keyof T>(
  t: T,
  k: K
): t is { [P in keyof T]: P extends K ? NonNullable<T[P]> : T[P] } {
  return t[k] != null;
}

The propNotNull(obj, key) guard will, if it returns true, narrow obj to a type in which obj.key is known not to be null (or undefined... just because NonNullable<T> is a standard utility type).

And now your a() function can be written as:

function aUserDefinedTypeGuard(par: {
  a: string;
  b: null | string;
}): { a: string; b: string } | undefined {
  if (!propNotNull(par, "b")) {
    return;
  } else {
    return par;
  }
}

The check !propNotNull(par, "b") causes par not to be narrowed at all in that first branch, but narrows par to {a: string; b: string} in the second branch. That's enough to make your code compiler without error.

But I don't know if it's worth the extra complexity compared to the type assertion.


Okay, hope that helps; good luck!

Link to code

Tags:

Typescript