Filtering Types with Correct Type Inference in RxJs

John Crowson
ngconf
Published in
4 min readFeb 28, 2019

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:

  1. 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/

--

--