Solving a common problem with type predicates

Solving a common problem with type predicates

Erick

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:

TS
const arr = ['TypeScript', 'is', 'cool', undefined]

So then of course I can just filter out the falsy values and be left with strings.

TS
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:

The inferred types of string and undefined

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.

inferred type after filter

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
TS
// 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

TS
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. Type now 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