When I use a lot of optional chaining in a row you still can't say to me in which part it becomes null
or undefined
so I can't understand the profit of typing this operator again and again
I'll try to spell out the problem using more concrete scenarios.
Say we have a codebase with a getPort()
function already in it. This getPort()
function will return the config -> server -> http -> port
property, or it will return 80 if something in that chain was nullish.
Our goal is to introduce a new function, getHost()
, that 1. throws an error if config
is nullish
, and 2. returns the value of config -> server -> host
(host
being a newly added required property of server
). This isn't a TypeScript codebase, so we have to read surrounding code to get an understanding of what types the data conforms to. In a healthy non-TypeScript codebase, there should be plenty of hints lift around so we don't have to hunt too long, as such, the task is to build this getHost()
function using only getPort()
as a reference. We also want to figure out if the getHost()
function might return a nullish that the caller has to deal with.
Hopefully the objectives will make more sense as we get into it.
Scenario 1
We walk into the codebase and find getPort()
defined as follows:
function getPort() {
return config?.server?.http?.port ?? 80;
}
How should we define getHost()
? Easy peasy, like this:
/** This function may return a nullish value */
function getHost() {
if (config == null) throw new Error('config cannot be nullish');
return config.server?.host;
}
Because the author of getPort()
explicitly placed a ?.
between each segment, we knew that the author expected that it's possible for any one of those segments to be a nullish value, therefor we too should expect the same. In getHost()
, we know config
won't be nullish (as we throw if it is), but config.server
still might be, so we need to use ?.
and warn callers that a nullish value could be returned.
Scenario 2
In a parallel universe, we walk into a codebase and find getPort()
defined as follows (note that this is subtly different from before, in one spot we're using .
instead of ?.
).
function getPort() {
return config?.server.http?.port ?? 80;
}
How should we define getHost()
this time? The code author explicitly told us that when config
is not nullish, config.server
isn't either, and we can use that information to build a getHost()
function that does not use ?.
, and that we know doesn't return a nullish
value.
function getHost() {
if (config == null) throw new Error('config cannot be nullish');
return config.server.host;
}
Scenario 3
In another universe, Object.get()
was standardized, and the codebase we walked into had a getPort()
function like this:
function getPort() {
return Object.get(config, 'server.http.port') ?? 80;
}
Now we have a problem. Did the author use Object.get()
because most of the segments in this config -> server -> http -> port
could be nullish (like in scenario 2) or because all of them could be nullish (like in scenario 1)? There's no way to tell. The correct implementation for getHost()
will likely either be what we see in scenario 1 or 2, but you can't tell which one to use just by looking at this getPort()
's implementation - you'll have to dig deeper into the codebase.
That's the sort of problem I'm describing. It's an extremely specific example, but it's part of a more general principle - the idea that, especially in non-TypeScript codebases, it's important to leave hints everywhere you reasonably can about the types of your data, so that future you/others maintaining the code don't have to go on deep hunts to find type information.