Constraints liberate, TypeScript edition

A little TypeScript puzzle I encountered recently:

interface Valid {
  readonly isValid: true;
}

interface Invalid {
  readonly isValid: false;
  message: string;
}

type Validation = Valid | Invalid;

function report(validation: Validation) {
  if (!validation.isValid) {
    console.error("Invalid", validation.message);
  }

  console.info("Valid");
}

Looks fine right? And the TypeScript playground has no problem with it. But in my local environment, it was complaining that the message property didn’t exist on validation, despite the fact that validation should be narrowed to Invalid in the if branch.

Eventually, I figured out that the problem was due to the local environment not having strictNullChecks set. And, if you make the effects of that explicit, by making the isValid fields optional:

interface Valid {
  readonly isValid?: true;
}

interface Invalid {
  readonly isValid?: false;
  message: string;
}

you can reproduce the error in the TypeScript playground. The problem is, if isValid can be undefined in Valid, then !isValid might be true for a Valid object, and so that possibility can’t be excluded in the if branch.

It’s counterintuitive, though, that making the compiler less strict can make your program stop compiling. I suppose this is an example of how constraints liberate, liberties constrain. Making the compiler stricter means it knows more about what your program is doing, so it can prove more code is correct.