Filtering Types with Correct Type Inference in RxJs
A common use of the RxJs filter operator is to only allow or prevent certain types from being passed to the next pipe operator or subscribe function.
const scrollEvents$ = router.events.pipe(
filter(event => event instanceof Scroll)
);
However, you’ll notice in the above snippet that scrollEvents$
is inferred to be of type Observable<Event>
instead of Observable<Scroll>
. This inability to correctly infer type is not an issue with RxJs, but rather Typescript (open issues: #10734 and #16069).
Review: Type Checking in Typescript
Feel free to skip this section if you are familiar with type checking in Typescript.
Primitives
You can use typeof
to check the primitive type:
typeof
is a unary operator that returns a string indicating the type of the unevaluated operand
const num = 5;typeof num; // returns 'number'typeof num === 'number' // returns true
Classes
You can use instanceof
to check the class type:
instanceof
is a binary operator, accepting an object and a constructor. It returns a boolean indicating whether or not the object has the given constructor in its prototype chain.
const toyota = new Car("Toyota");toyota instanceof Car; // returns true
Using typeof
won’t work:
const toyota = new Car("Toyota");typeOf toyota; // returns 'object' :(
Interfaces
Unfortunately, you can’t use instanceof
because interfaces don't exist at runtime. You also can’t use typeof
, because you’ll get back 'object',
like with classes.
You will need to check if a distinct property or function of the interface is defined:
interface Fish {
canSwim: boolean;
color: string;
}interface Bird {
canFly: boolean;
wingLength: number;
}const pet = { canSwim: true, color: 'green' };(<Fish>pet).canSwim !== undefined; // returns true, so pet is a fish
User-Defined Type Guards
We can utilize a user-defined type guard in Typescript to encapsulate these.
A type guard is some expression that performs a runtime check that guarantees the type in some scope.
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).canSwim !== undefined;
}const pet = { canSwim: true, color: 'green' };if (isFish(pet)) {
// Type is now inferred to be Fish within the scope
console.log(pet.color);
}
Note: We could have the pet parameter of isFish
accept any
, but that would provide more opportunity to provide another interface that also has a canSwim
property, causing the type guard to return true incorrectly.
Filtering Primitive, Interfaces, and Classes
Option 1: Filter, then Cast
The first option is to add an additional map that casts the Event
class after the filter:
const scrollEvents$ = router.events.pipe(
filter(event => event instanceof Scroll),
map(event => event as Scroll)
)
.subscribe(event => console.log(event.position));
Now, in subscribe, the event is correctly inferred of type Observable<Scroll>
.
If we want to reuse this same type inferencing filter again, we can create a custom RxJs operator:
export function isScrollEvent<Scroll>() {
return (source$: Observable<Event>) => source$.pipe(
filter(event => event instanceof Scroll),
map(event => event as Scroll)
);
}const scrollEvents$ = router.events.pipe(
isScrollEvent()
).subscribe(event => console.log(event.position));
For primitives or interfaces, we just need to update the filter to use typeof
or an undefined check, respectively.
Option 2: Define a Type Guard
The second option is to define a type guard to correct the type inference:
export function inputIsScroll(input: Event): input is Scroll {
return input instanceof Scroll;
}const scrollEvents$ = router.events.pipe(
filter(inputIsScroll)
)
.subscribe(event => console.log(event.position));
Now, in subscribe, the event is correctly inferred of type Observable<Scroll>
.
We could abstract this approach further by creating a custom RxJs operator:
function inputIsScroll(input: Event): input is Scroll {
return input instanceof Scroll;
}export function isScrollEvent<Scroll>() {
return (source$: Observable<Event>) => source$.pipe(
filter(inputIsScroll)
);
}const scrollEvents$ = router.events.pipe(
isScrollEvent()
)
.subscribe(event => console.log(event.position));
Filtering Null and Undefined
If you search the internet for ways to filter out null and undefined, you’ll likely get one of two suggested solutions:
filter(Boolean)
const source: Observable<string> = of(undefined, 'hello', null);source.pipe(filter(Boolean)).subscribe(str =>
console.log(str.length)
);
Unfortunately, we lose the type inference, and str
is of type any
.
2. filter(x => x !== null && x !== undefined)
const source: Observable<string> = of(undefined, 'hello', null);source.pipe(
filter(str => str !== null && str !== undefined)
)
.subscribe(str => console.log(str.length));
Unfortunately, this does not work if you are using --strictNullChecks
.
Solution
function inputIsNotNullOrUndefined<T>(input: null | undefined | T): input is T {
return input !== null && input !== undefined;
}export function isNotNullOrUndefined<T>() {
return (source$: Observable<null | undefined | T>) =>
source$.pipe(
filter(inputIsNotNullOrUndefined)
);
}
We can now infer the correct type with --strictNullChecks
:
const source: Observable<string | null | undefined> = of(undefined, 'hello', null);source.pipe(
isNotNullOrUndefined()
)
.subscribe(str => console.log(str.length));
EnterpriseNG is coming November 4th & 5th, 2021.
Come hear top community speakers, experts, leaders, and the Angular team present for 2 stacked days on everything you need to make the most of Angular in your enterprise applications.
Topics will be focused on the following four areas:
• Monorepos
• Micro frontends
• Performance & Scalability
• Maintainability & Quality
Learn more here >> https://enterprise.ng-conf.org/