'TypeScript: Why is that my filter method cannot narrow the type and eliminate the undefined and false from the array

I have this type from the d.ts file of an API

type EventConfigurations = {
  Event1: {
    Enabled?: boolean;
  };

  Event2: {
    Enabled?: boolean;
  };

  Event3: {
    Enabled?: boolean;
  };
};

Now that I have this data coming back from the response of that API call

const eventConfigurations: EventConfigurations = {
  Event1: { Enabled: true },

  Event2: { Enabled: false },

  Event3: { Enabled: true }
};

and now I want to map the data to


enum EventDesc {
  Event1 = "XXX",
  Event2 = "YYY",
  Event3 = "ZZZ"
}

type MyEvent = {
  eventDesc: EventDesc;
  topic: EventTopic;
};

const eventConfigurations: EventConfigurations = {
  Event1: { Enabled: true },
  Event2: { Enabled: false },
  Event3: { Enabled: true }
};

const events: MyEvent[] = (Object.keys(eventConfigurations) as Array<
  keyof EventConfigurations
>)

  .map(
    (eventType) =>
      eventConfigurations[eventType].Enabled &&
      ({
        eventDesc: EventDesc[eventType],
        topic: EventTopic[eventType]
      } as MyEvent)
  )

  .filter(Boolean);

And the compiler raised an error saying that, the events could be either

(false | MyEvent | undefined)[]

But I added .filter(Boolean); at the end of the map, it should eliminate the possibilities of it being either false or undefined.

Here is the live demo https://codesandbox.io/s/thirsty-yonath-ttdhn?file=/src/index.ts



Solution 1:[1]

TypeScript's standard library does have a signature for Array.prototype.filter() which will narrow the type of the array, but it will only use that signature if the compiler recognizes the passed-in callback as a type guard function whose return type is a type predicate of the form paramName is SomeType. And, unfortunately, the compiler is not currently able to infer that a callback (like Boolean or x => !!x) is a type guard; you have to annotate it as such.

(See microsoft/TypeScript#16069 for the feature request to use some sort of control flow analysis to interpret certain functions as type guards.)

Here's one such annotated callback:

const truthyFilter = <T>(x: T | false | undefined | null | "" | 0): x is T => !!x;

or if you really want you can use Boolean as the implementation with a type assertion like

const truthyFilter2 = Boolean as any as <T>(x: T | false | undefined | null | "" | 0) => x is T;

That is sort of a generic truthiness detector, although TypeScript doesn't really have a good way to represent all possible falsy values in the type system (NaN is unrepresentable for exaple). Anyway you can see it in action like this:

function fn(arr: Array<false | undefined | MyEvent>) {

  const item = arr[0];
  if (truthyFilter(item)) {
    item.eventDesc; // okay, no error
  }

And now if you use it with filter() the compiler will narrow it as expected:

  const myEvents = arr.filter(truthyFilter)
  // const myEvents: MyEvent[]

Okay, hope that helps; good luck!

Playground link

Solution 2:[2]

You'll want to use a type guard:

const filterUnwanted = (value: any): value is MyEvent => ('eventDesc' in value && 'topic' in value)

Or instead of Array#map followed by Array#filter, consider Array#reduce:

const events = (Object.keys(eventConfigurations) as Array<keyof EventConfigurations>)
  .reduce((acc, eventType) => (
    eventConfigurations[eventType].Enabled ? acc.concat([{
      eventDesc: EventDesc[eventType],
      topic: EventTopic[eventType],
    }]) : acc), [] as MyEvent[],
  );

Solution 3:[3]

There's two approaches to this:

Option 1:

Return an empty {} as MyEvent when condition in .map() is false, because currently undefined is returned. And then perform filter after.

const events : MyEvent[] = (Object.keys(eventConfigurations) as Array<
  keyof EventConfigurations
>).map((eventType) =>
      (eventConfigurations[eventType].Enabled ?
      ({
        eventDesc: EventDesc[eventType],
        topic: EventTopic[eventType]
      } as MyEvent) : {} as MyEvent) // Return an empty object if Enabled == false
  ).filter(performFilter); // filter by checking if objects returned are empty or not

const performFilter = (obj : MyEvent) => Object.keys(obj).length > 0;

Option 2:

Filter the list before you use map to create new objects.

const events: MyEvent[] = (Object.keys(eventConfigurations) as Array<
  keyof EventConfigurations
>)
.filter((event)=>eventConfigurations[event].Enabled)
.map(
  (eventType) => ({
      eventDesc: EventDesc[eventType],
      topic: EventTopic[eventType]
    } as MyEvent)
);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3