SergeyS
SergeyS

Reputation: 4510

TypeScript filter out nulls from an array

TypeScript, --strictNullChecks mode.

Suppose I have an array of nullable strings (string | null)[]. What would be a single-expression way to remove all nulls in a such a way that the result has type string[]?

const array: (string | null)[] = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = ???;

Array.filter does not work here:

// Type '(string | null)[]' is not assignable to type 'string[]'
array.filter(x => x != null);

Array comprehensions could've work but they are not supported by TypeScript.

Actually the question can be generalized to the problem of filtering an array of any union type by removing entries having one particular type from the union. But let's focus on unions with null and perhaps undefined as these are the most common usecases.

Upvotes: 390

Views: 216264

Answers (22)

Bijou Trouvaille
Bijou Trouvaille

Reputation: 9434

You can use a type predicate function in the .filter to avoid opting out of strict type checking:

function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
    return value !== null && value !== undefined;
}

const array: (string | null)[] = ['foo', 'bar', null, 'zoo', null];
const filteredArray: string[] = array.filter(notEmpty);

Typescript 5.5+ knows how to infer the predicate type, so now you can simply write this:

const filteredArray: string[] = array.filter(x => x !== null)

Alternatively, you can use array.reduce<string[]>(...).

Rigorous predicates

While the above solution works in most scenarios, especially when the predicate type is automatically inferred, you can get a more rigorous type check in the predicate. As presented, the function notEmpty does not actually guarantee that it identifies correctly whether the value is null or undefined at compile time. For example, try shortening its return statement down to return value !== null;, and you'll see no compiler error, even though the function will incorrectly return true on undefined.

One way to mitigate this is to constrain the type first using control flow blocks, and then to use a dummy variable to give the compiler something to check. In the example below, the compiler is able to infer that the value parameter cannot be a null or undefined by the time it gets to the assignment. However, if you remove || value === undefined from the if condition, you will see a compiler error, informing you of the bug in the example above.

function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
  if (value === null || value === undefined) return false;
  const testDummy: TValue = value;
  return true;
}

A word of caution: there exist situations where this method can still fail you. Be sure to be mindful of issues associated with contravariance.

Upvotes: 429

alukach
alukach

Reputation: 6288

Similar to @bijou-trouvaille's answer, you just need to declare the <arg> is <Type> as the output of the filter function:

array.filter((x: MyType | null): x is MyType => x !== null);

Upvotes: 252

kjetilh
kjetilh

Reputation: 4976

Solution from TS 5.5

In TS 5.5 you'll be able to do arr.filter(x => x != null)! 🎉

Code example from Mateusz Burzynski


Alternative solution:

If you can accept the overhead of another .map() an elegant solution is using the Non-null assertion operator.

const array = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = array.filter(s => s != null).map(s => s!);

If you'd like to keep the undefines you can use typeof on the variable and the utility type Exclude to remove nulls from the type.

const array = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = array
  .filter(s => s !== null)
  .map(s => s as Exclude<typeof s, null>);

Upvotes: 6

DLight
DLight

Reputation: 1825

One liner:

const filteredArray: string[] = array.filter((s): s is string => Boolean(s));

TypeScript playground

The trick is to pass a type predicate (:s is string syntax).

This answer shows that Array.filter requires users to provide a type predicate.

Note: replace Boolean(s) with s !== null if you don't want to remove falsy values, like empty strings "".

Upvotes: 47

Ivan Sanz Carasa
Ivan Sanz Carasa

Reputation: 1387

You can use the is construct to narrow the type down:

enter image description here

enter image description here

enter image description here

enter image description here

Upvotes: 12

Karba
Karba

Reputation: 1

Or you can try the package: @p4ck93/ts-is

https://www.npmjs.com/package/@p4ck493/ts-is

The example uses the CDN method, but the package also supports typescript.

<script>var exports = {};</script>
<script src="//unpkg.com/@p4ck493/[email protected]/dist/index.js"></script>
<script>
    const {is} = exports;
    console.log('is.string: ', is.string('')); // true
    console.log('is.string.empty: ', is.string.empty('')); // true
    console.log('is.string.not.empty: ', is.string.not.empty('')); // false
    
    
   const array = ["foo", "bar", null, "zoo", null];
   const filterdArray = array.filter(is.string.not.empty);
   
   console.log('array:', array);
   console.log('filterdArray:', filterdArray);
</script>

UPD

Or TypeScript:

import {is} from '@p4ck493/ts-is';

const array = ["foo", "bar", null, "zoo", null];
const filterdArray = array.filter(is.string.not.empty);

/**
Alternative:

array.filter(is.not.null);
array.filter(is.not.empty);
array.filter(is.string);

**/

Upvotes: -2

BadPirate
BadPirate

Reputation: 26177

Combining one of my favorite answers above, with some of the generic tricks and an extension to the Array interface, I was able to make a global define that after adding to your module allows for any array to be "squished" removing all null values replacing (any|undefined|null)[] with any[].

Like so: mixedArray.squish() good for chaining and map.

Just add this code somewhere in your module (feel free to leave out the eslint stuff, but my set bugged me about a few things here):

/* eslint-disable no-unused-vars */
/* eslint-disable no-extend-native */
declare global {
  interface Array<T> {
    squish<NonNull, Nullable extends (NonNull | undefined | null)>(): NonNull[];
  }
}

if (!Array.prototype.squish) {
  Array.prototype.squish = function squish<NonNull, T extends(NonNull|undefined|null)>
  (this: T[]): NonNull[] {
    return this.flatMap((e) => (e ? [e] : [])) as NonNull[]
  }
}

Upvotes: -1

std4453
std4453

Reputation: 858

Just realized that you can do this:

const nonNull = array.filter((e): e is Exclude<typeof e, null> => e !== null)

So that you:

  1. get a one-liner, no additional functions
  2. do not have to know the type of array elements, so you can copy this everywhere!

Upvotes: 57

A N D J I
A N D J I

Reputation: 29

The shortest way:

const validData = array.filter(Boolean)

Upvotes: 0

Simon_Weaver
Simon_Weaver

Reputation: 145950

I've come back to this question many times hoping some new Typescript feature or typing may fix it.

Here's a simple trick I quite like for when combining map with a subsequent filter.

const animals = ['cat', 'dog', 'mouse', 'sheep'];

const notDogAnimals = animals.map(a => 
{
   if (a == 'dog')
   {
      return null!;   // just skip dog
   }
   else {
      return { animal: a };
   }
}).filter(a => a);

You'll see I'm returning null! which actually becomes type never - meaning that the final type doesn't have null.

This is a slight variation on the original question but I find myself in this scenario quite often and it helps avoid another method call. Hopefully someday Typescript will come up with a better way.

Upvotes: 0

Mathias Dpunkt
Mathias Dpunkt

Reputation: 12184

Here is a solution that uses NonNullable. I find it even a little bit more concise than the accepted answer by @bijou-trouvaille

function notEmpty<TValue>(value: TValue): value is NonNullable<TValue> {
    return value !== null && value !== undefined;
}
const array: (string | null | undefined)[] = ['foo', 'bar', null, 'zoo', undefined];

const filteredArray: string[] = array.filter(notEmpty);
console.log(filteredArray)
[LOG]: ["foo", "bar", "zoo"]

Upvotes: 14

Alan Draper
Alan Draper

Reputation: 397

const filterdArray = array.filter(f => !!f) as string[];

Upvotes: -1

frouo
frouo

Reputation: 5223

Using reduce

Some answers suggest reduce, here is how:

const languages = ["fr", "en", undefined, null, "", "de"]

// the one I prefer:
languages.reduce<string[]>((previous, current) => current ? [...previous, current] : previous, [])

// or
languages.reduce((previous, current) => current ? [...previous, current] : previous, Array<string>())

// or
const reducer = (previous: string[], current: string | undefined | null) => current ? [...previous, current] : previous
languages.reduce(reducer, [])

Result: ["fr", "en", "de"]

TS Playground here.

Upvotes: 1

Mike Sukmanowsky
Mike Sukmanowsky

Reputation: 4477

One more for good measure as people often forget about flatMap which can handle filter and map in one go (this also doesn't require any casting to string[]):

// (string | null)[]
const arr = ["a", null, "b", "c"];
// string[]
const stringsOnly = arr.flatMap(f => f ? [f] : []);

Upvotes: 316

GG.
GG.

Reputation: 21844

TypeScript has some utilities to infer the type of the array and exclude the null values from it:

const arrayWithNulls = ["foo", "bar", null, "zoo", null]

type ArrayWithoutNulls = NonNullable<typeof arrayWithNulls[number]>[]

const arrayWithoutNulls = arrayWithNulls.filter(x => x != null) as ArrayWithoutNulls

Longer but safer than just manually casting as string[] on your new array.

Step by step:

  1. Get the types from the original array:
typeof arrayWithNulls[number] // => string | null
  1. Exclude the null values:
NonNullable<typeof arrayWithNulls[number]> // => string
  1. Make it an array:
NonNullable<typeof arrayWithNulls[number]>[] // => string[]

Links:

Upvotes: 0

shrijan tripathi
shrijan tripathi

Reputation: 163

simply use

array.filter(Boolean);

This will work for all truth values.

This, unfortunately, do not provide type inference, found this solution on here


type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T; //from lodash 

function truthy<T>(value: T): value is Truthy<T> {
    return Boolean(value);  //  or !!value
}

const arr =["hello","felow","developer","",null,undefined];

const truthyArr = arr.filter(truthy);

// the type of truthyArr will be string[]

Upvotes: 3

David
David

Reputation: 2748

If you already use Lodash, you can use compact. Or, if you prefer Ramda, the ramda-adjunct has also compact function.

Both have types, so your tsc will be happy and get the correct types as a result.

From Lodash d.ts file:

/**
 * Creates an array with all falsey values removed. The values false, null, 0, "", undefined, and NaN are
 * falsey.
 *
 * @param array The array to compact.
 * @return Returns the new array of filtered values.
 */
compact<T>(array: List<T | null | undefined | false | "" | 0> | null | undefined): T[];

Upvotes: 8

Akitha_MJ
Akitha_MJ

Reputation: 4294

If you are checking null with other conditions using filter simply this can be used hope this helps for some one who is looking solutions for an object array

array.filter(x => x != null);
array.filter(x => (x != null) && (x.name == 'Tom'));

Upvotes: 0

Robert Massaioli
Robert Massaioli

Reputation: 13477

To avoid everybody having to write the same type guard helper functions over and over again I bundled functions called isPresent, isDefined and isFilled into a helper library: https://www.npmjs.com/package/ts-is-present

The type definitions are currently:

export declare function isPresent<T>(t: T | undefined | null): t is T;
export declare function isDefined<T>(t: T | undefined): t is T;
export declare function isFilled<T>(t: T | null): t is T;

You can use this like so:

import { isDefined } from 'ts-is-present';

type TestData = {
  data: string;
};

const results: Array<TestData | undefined> = [
  { data: 'hello' },
  undefined,
  { data: 'world' }
];

const definedResults: Array<TestData> = results.filter(isDefined);

console.log(definedResults);

When Typescript bundles this functionality in I'll remove the package. But, for now, enjoy.

Upvotes: 11

Sourodeep Chatterjee
Sourodeep Chatterjee

Reputation: 197

I think this will be an easy approach, with more cleaner code

const array: (string | null)[] = ['foo', 'bar', null, 'zoo', null];
const filteredArray: string[] = array.filter(a => !!a);

Upvotes: 1

Nitzan Tomer
Nitzan Tomer

Reputation: 164137

You can cast your filter result into the type you want:

const array: (string | null)[] = ["foo", "bar", null, "zoo", null];
const filterdArray = array.filter(x => x != null) as string[];

This works for the more general use case that you mentioned, for example:

const array2: (string | number)[] = ["str1", 1, "str2", 2];
const onlyStrings = array2.filter(x => typeof x === "string") as string[];
const onlyNumbers = array2.filter(x => typeof x === "number") as number[];

(code in playground)

Upvotes: 14

Digvijay
Digvijay

Reputation: 25

I believe you have it all good except that the type checking just makes the filtered type not be different than the return type.

const array: (string | null)[] = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = array.filter(f => f !== undefined && f !== null) as any;
console.log(filterdArray);

Upvotes: 0

Related Questions