Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript discriminated union types with Rx.js filter operator?

Typescript supports discriminated unions. How to extend the same concept with Rxjs to the filter operator in below example?

interface Square {
    kind: 'square';
    width: number;
}

interface Circle {
    kind: 'circle';
    radius: number;
}

interface Center {
    kind: 'center';
}

type Shape = Square | Circle | Center;

const obs$: Observable<Shape> = of<Shape>({ kind: 'square', width: 10 });

// Expected type: Observable<Square>
// Actual type: Observable<Shape>
const newObs$ = obs$.pipe(
    filter((x) => x.kind === 'square')
);

I above code snippet, I would want to see newObs$ to have its type inferred as: Observable<Square>. But apparently, TypeScript doesn't do that.

How to achieve this? Am I reaching the limits of TypeScript type inference?

I look for this as it seems to be very useful in a Redux + Redux-Observable codebase.

like image 465
Harshal Patil Avatar asked May 25 '18 08:05

Harshal Patil


People also ask

What is discriminated unions in TypeScript?

The concept of discriminated unions is how TypeScript differentiates between those objects and does so in a way that scales extremely well, even with larger sets of objects. As such, we had to create a new ANIMAL_TYPE property on both types that holds a single literal value we can use to check against.

Can I check a type against a union type in TypeScript?

We can differentiate between types in a union with a type guard. A type guard is a conditional check that allows us to differentiate between types. And in this case, a type guard lets us figure out exactly which type we have within the union.

What is union type variable in TypeScript?

In TypeScript, we can define a variable which can have multiple types of values. In other words, TypeScript can combine one or two different types of data (i.e., number, string, etc.) in a single type, which is called a union type.


2 Answers

Actually you can do this with TypeScript type guards. See section "Type Guards and Differentiating Types" at http://www.typescriptlang.org/docs/handbook/advanced-types.html

The key here is the function isWhatever(x: any): x is Whatever => ... syntax.

This basically says that if the isWhatever function returns true then it guarantees that x is of Whatever type.

In your example TypeScript considers all three classes:

without-typeguard

So you can define the predicate function for filter() as this:

filter((x: Shape): x is Square => x.kind === 'square')

Now it will properly consider only the Square class:

with-typeguard

See live demo: https://stackblitz.com/edit/rxjs6-demo-z9lwxe?file=index.ts

Very similar question: https://github.com/ReactiveX/rxjs/issues/2340

like image 173
martin Avatar answered Oct 20 '22 09:10

martin


This is not necessarily a limitation with the TypeScript type system but rather with the implementation of filter. You can easily achieve the desired behaviour using flatMap:

// Inferred type: Observable<Square>
const newObs$ = obs$.pipe(
  flatMap((x) => x.kind === "square" ? of(x) : empty())
);
like image 21
Scraph Avatar answered Oct 20 '22 11:10

Scraph