You have probably heard null described as the billion-dollar mistake. But null itself is not inherently bad. Making null invisible is what tends to cause issues.
In Java, any reference type can be null. A variable declared as
string might contain a string at run time, or it might contain
null. You can’t be sure which one it is until you check. So if you want to avoid
NullPointerException, your code must be littered with checks for null.
Modern type systems in languages like C#, Typescript, and Swift can make null more visible (at compile time rather than at runtime). With strict checks enabled, you can make null a part of an interface. So a variable declared as
string can only contain a string. If you want it to be nullable, you have to explicitly add it to the declaration (hence making the null visible).
This works well enough, but you have to be careful not to inadvertently make null invisible again by storing some type-compatible nullish value. If your interface expects a non-null string, chances are you want it to be a non-blank string as well. But most type systems are not that granular.
It may simply be that the perfect type system does not exist, and you’ll just have to cover these cases with tests. Or you could settle for a few runtime checks. You can limit the number of checks needed by devising a custom type that does some validation when it’s created.
In Typescript, “branded types” are often used for this. For instance, instead of using
string for an ID type, you can create a
UserID type that can only be constructed by a specific function that ensures it is not empty.
Null Object Pattern
There is a name for the pattern of “storing some type-compatible nullish value.” It’s called the null object pattern. It is certainly one way to avoid crashes when dereferencing null. In this pattern, you don’t use a language singleton like
null to represent missing values. Instead, you assign a type-specific nullish value. Usually, this is an object that conforms to the same interface, performing a no-op for all methods.
nil behaves like a null object. You can call any method on
nil, and it will simply do nothing, with no crashes at all. It is, of course, nice to avoid having your program crash. But silently absorbing spurious method calls tends to bury logic errors. A crash is at least a very noticeable indication of a bug to fix.
null in the type system is a good balance. Your interfaces can communicate which values are required, and which are optional. And a single representation of null enables standard language features for handling them. For example, you’re able to use a null-coalescing operator instead of having to compare against a type-specific nullish value.
undefined. At least Typescript seems to have standardized around the use of
undefined for absent values. If you do the same, you can take advantage of Typescript’s language features for dealing with undefined.
But whatever language you’re using, keep those nulls where you can see ’em.