Guard clause and exhaustiveness checking
Nested conditionals suck. They're hard to write and even harder to read. I've rarely regretted the time I've spent optimizing for the flattest conditional structure in my code. The following piece mimics the actions of a traffic signal:
The snippet above suffers from two major issues:
- It contains three contiguous levels of nested conditionals.
- The conditionals don't cover the case where the return value is undefined.
- If you add a fourth member to the Signal enum, now the processing function doesn't exhaustively cover all the cases and it won't communicate that fact with you.
We can leverage guard clauses to fix the first two issues.
The guard (clause) provides an early exit from a subroutine, and is a commonly used deviation from structured programming, removing one level of nesting and resulting in flatter code: replacing if guard { ... } with if not guard: return; ...
We can rewrite the earlier snippet as follows:
This model has a flatter structure and now it's gracefully handling the undefined return path. However, the third issue still persists. In an alien world, if someone added a fourth member to the Signal enum, that'd make the conditional flow in the processSignal function incomplete since it wouldn't be covering that newly added fourth enum member. In that case, the above snippet will execute the final catch-all conditional statement; not something that we'd want.
TypeScript provides a never type to throw a compilation error if a new member isn't covered by the conditional flow. Here's how you'd leverage it:
Ideally, the assertNever should never be called. Try removing a conditional and see how TypeScript starts screaming at you regarding the unhandled case. The assertNever function will also raise a runtime error if any case remains unhandled.
Example in Python
The same idea can be demonstrated in Python using Python3.10's match statement and typing.NoReturn type.
Similar to TypeScript, mypy will complain if you add a new member to the enum but forget to handle that in the processor function. Python 3.11 added the Never type and assert_never function to the typing module. Underneath, Never is an alias to the NoReturn type; so you can use them interchangeably. However, in this case, Never seems to communicate the intent better. You may also choose to use the backported versions of the type and function from the typing_extensions module. Here's how:
Further reading
Discussion in the ATmosphere