DEV Community

Lukas Bach
Lukas Bach

Posted on • Originally published at Medium

TypeScript: Properly filtering arrays for non-null values

If you have used JavaScript and array operations for a while, there is a good chance you wanted to filter an array for non-null values. And if you are using TypeScript, there is a chance you ended up with something along the lines of that:

const array = ["Michael", "Dwight", undefined, "Pam", "Jim", undefined]
typeof array // (string | undefined)[]

console.log(array.map(value => value.includes("Mich")))
// error, value could be undefined
// I should filter for only defined values...

const filteredAttempt = array.filter(value => !!value)
typeof filteredAttempt // (string | undefined)[]
// Argh dang, TypeScript doesn't infer the filter logic, that didn't do anything...

// screw it
const filteredAttempt2 = array.filter(value => !!value) as any
typeof filteredAttempt2 // any, good enough..
console.log(filteredAttempt2.map(value => value.includes("Mich")))
Enter fullscreen mode Exit fullscreen mode

You will quickly find that TypeScript doesn't actually check what the filter function does, so the returned array will be the same, and you will have to straighten the type manually to make it work, often relying on any which is always a bad idea.

However, this is really easy to do properly with type guards. A type guard function in TypeScript that takes a parameter x and returns a type of the form x is T with T being any type. Now, if code is called in a scope in which this type guard function was called and returned true, the type of x will be automatically narrowed down to T within that scope:

const isString = (v: any): v is string =>
    typeof v === "string"

const stringOrNumber = Math.random() > .5 ? "hello" : 42

if (isString(stringOrNumber)) {
    console.log(stringOrNumber.endsWith("lo")) // works
} else {
    console.log(stringOrNumber.toPrecision()) // works
}
Enter fullscreen mode Exit fullscreen mode

Note that this technically is a type conversion, and you very much can do non sensical stuff with this:

const surelyAString = (v: any): v is string => true

const stringOrNumber = Math.random() > .9999 ? "hello" : 42

if (surelyAString(stringOrNumber)) {
    console.log(stringOrNumber.endsWith("lo")) 
    // works, but probably not during runtime
}
Enter fullscreen mode Exit fullscreen mode

However, it allows you to encapsulate casting logic, which makes the code easier to read and understand which conversions are being made and how they work, thus reducing the likelihood of errors in that logic.

Returning to the original question: How can we leverage type guards to properly filter an array for non-undefined values?

const array = ["Michael", "Dwight", undefined, "Pam", "Jim", undefined]
typeof array // (string | undefined)[]

const isNotUndefined = <T>(value: T | undefined): value is T => 
    value !== undefined

const filteredArray = array.filter(isNotUndefined)
typeof filteredArray // string[]

console.log(filteredArray.map(value => value.includes("Mich"))) // works!
Enter fullscreen mode Exit fullscreen mode

We define a type guard function isNotUndefined with generic type T, which takes a parameter of type T | undefined. If we now supply a value of type string | undefined, T is inferred to be string, and the type guard will narrow the type of the parameter down to string if it returns true. This can also be done for filtering out other types such as null.

const isNotNullish = <T>(value: T | null | undefined): value is T => 
    value !== undefined && value !== null
Enter fullscreen mode Exit fullscreen mode

Top comments (0)