2021-09-18
|~2 min read
|335 words
When writing an interface for a function, it’s common to want to allow a range of types. This can often be communicated with a generic.
For example, imagine a function toString
:
function toString<T>(x: T): string {
return x.toString()
}
Our function will take a type T
and returns a string
. In Javascript, the Object prototype includes a toString
method and (almost) everything in Javascript is an object. Typescript is now smart enough to know that not every T
has a toString
method on its prototype, so the compiler will complain about this function now, but it wouldn’t about a variant - for example:
function toString<T>(x: T): string {
return String(x)
}
In this case, we’re saying we’ll take any T
and convert it to a string
. However, interestingly, this includes null
and undefined
:
toString(2)
toString("foo")
toString(undefined)
toString(null)
These work by returning "undefined"
and "null"
- which probably isn’t what we want.
One way to solve this is with the NonNullable
utility type1:
function totalToString<T extends {}>(x: NonNullable<T>): string {
return x.toString()
}
Now, when we try to pass undefined
or null
into our function, Typescript will alert us that they’re not allowed and so we shouldn’t call the function:
totalToString(undefined)
totalToString(null)
totalToString(2)
In this way, we’ve gone from a Partial function, one in which all allowed inputs may not return a value, into a Total function, one which terminates in a value for all possible inputs.
We’ve also done it in a way where we know up front whether we’re passing in undefined
or null
and can handle that - rather than getting a string of "undefined"
that looks just like any other string.
H/t to Lauren Tan for her 2018 DotJS talk Learning To Love Type Systems which introduced me to the NonNullable
utility as well as the distinction between Partial and Total functions.
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!