Solving a common problem with type predicates
Written by Erick /
Earlier today, I saw a click-baity video sayiing that they've fixed TypeScript. What they did is write a small library for an annoyance that we'll see shortly. Now, TypeScript definitely has its quirks, but I this most of that is due to needing to avoid "breaking" legacy JavaScript code. I also think that installing a library for something like this should really only be done once you understand why it's behaving that way.
The problem this person had shoId something similar to this in its example:
const arr = ['TypeScript', 'is', 'cool', undefined]
So then of course I can just filter out the falsy values and be left with strings.
const filteredWithBoolean = arr.filter(Boolean)
// ["TypeScript", "is", "cool"]
The pain point is that, in TypeScript, even after filtering out the falsy stuff here in filteredWithBoolean
, under the hood, the union of string
and undefined
remains the inferred type:
This doesn't seem like much of a problem, until you try to do something like get the length of the string at index 0, for example.
This error occurs since undefined can never have a length, and though index 0 does not hold undefined, it's technically possible that at some point it could.
The fix
TypeScript loves to be reassured. Though I filtered out non-truthy things, the array itself can potentially let an undefined type
into the array club.
To fix this, I'll try something new with the array; instead of passing Boolean
to each item, I'll write a function to be used as a callback within filter that will verify the type of each object; if that type is a string, I vouch for it with a type predicate and TypeScript believes us.
Type predicate in action
// traditional function version
function onlyStrings(maybeString: unknown): maybeString is string {
return typeof maybeString === 'string'
}
// arrow function version
const onlyStrings = (maybeString: unknown): maybeString is string => typeof maybeString === 'string'
Notice that the return type above is maybeString is string
This explicity tells TypeScript that we've checked out its creds and they are who they say.
Putting it to the test
const arr = ['TypeScript', 'is', 'cool', undefined]
const filteredWithTypePredicate = arr.filter(onlyStrings)
// ["TypeScript", "is", "cool"]
Voilà ! Now the type is inferred as an array of strings.
Keep in mind that when you're defining your own type guards, it's possible to lead the compiler astray. If you're doing this in enterprise / production grade code, of course ensure that it's thoroughly tested or find a better, maybe slightly less elegant and more rigid solution.
🍻 Erick