Constraints liberate, TypeScript edition
01 Apr 2022A 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.