'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!
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 |