Typesafe view models with RxJS and Angular
In this article, we are going to improve the code from
a previous blog article by making the view model typesafe.
We will do this with an NPM package specially created for this article
called @bryanhannes/typed-view-model
.
In this NPM package is a function that will help us to create typesafe view models.
Forking the repository
We are going to use the Car catalog application from a previous article. We can fork the repository
from Stackblitz
or GitHub. In this article, we are going to use
the Stackblitz repository
.
Installing the NPM package
After forking the repository, we have to install the NPM package @bryanhannes/typed-view-model
.
In StackBlitz we can do this by adding @bryanhannes/typed-view-model
to the dependencies
section.
If you checked out the source code from Github, you can install the package with:
npm install @bryanhannes/typed-view-model
Typesafe view model
This @bryanhannes/typed-view-model
NPM package exposes 1 function called vm<T>()
. vm
stands for view model
.
So let’s import this function in the app.component.ts
.
import {vm} from '@bryanhannes/typed-view-model';
Now we want to use this vm
function to create the vm$
observable. In the documentation of the NPM package is shown
the following example:
interface PageViewModel {
name: string;
number: number;
stringArray: string[];
}
const name$$ = new Subject<string>();
const number$$ = new Subject<number>();
const stringArray$$ = new Subject<string[]>();
const vm$ = vm<PageViewModel>({
name: {observable: name$$, initialValue: 'John Snow'},
number: {observable: number$$, initialValue: 5000},
stringArray: {observable: stringArray$$, initialValue: ['a', 'b', 'c']},
});
From the example, we can see
- that the
vm()
function takes in an object with the same property keys asPageViewModel
. If we add a property that is not inPageViewModel
we get a compilation error. That is the typesafe view model we are talking about. - that the value object has 2 properties
observable
should be of typeObservable<K>
whereK
is of the same type as the corresponding property in thePageViewModel
initialValue
should be of the same type as the corresponding property in thePageViewModel
interface PageViewModel {
name: string;
}
const color$$ = new Subject<string>();
const number$$ = new Subject<number>();
// This does not compile
const vm$ = vm<PageViewModel>({
color: {observable: color$$, initialValue: ''}, // Example 1: This line will not compile because 'color' is not a property of `PageViewModel`
name: {
observable: number$$, // Example 2: Does not compile because the type of the Subject is not assignable to the type of the property. Observable<number> is not assignable to Observable<string>
initialValue: [], // Example 3: Does not compile because a string is expected here and not an array
},
});
Now let’s test this vm()
function out.
First, let’s refactor the name queryParams$
to filter$
because this name is more appropriate.
We add the vm()
function in the same manner as in the example.
We also added map
operators to map the fields of CarFilter
to the corresponding property.
// Renamed from `queryParams$` to `filter$`
private readonly
filter$: Observable < CarFilter > =
this.activatedRoute.queryParams.pipe(
map((params: Params) => ({
name: params.name,
brand: params.brand,
color: params.color,
}))
);
...
public readonly
vm$ = vm<PageViewModel>({
name: {
observable: this.filter$.pipe(map((filter) => filter.name)), // Only need the name property here
initialValue: null,
},
color: {
observable: this.filter$.pipe(map((filter) => filter.color)), // Only need the color property here
initialValue: null,
},
brand: {
observable: this.filter$.pipe(map((filter) => filter.brand)), // Only need the brand property here
initialValue: null,
},
results: {observable: this.results$, initialValue: []},
});
Congratulations, we just created a typesafe view model!
Creating custom RxJS map
operators
A final improvement we can do to improve the readability of our code is adding custom RxJS map
operators.
// The first generic of `UnaryFunction` is the type of the value that is passed in the operator
// The second generic of `UnaryFunction` is the return type of the operator
// In the `mapToName` operator we pass in a `CarFilter` and return `string` or `null`
export const mapToName = (): UnaryFunction<Observable<CarFilter>,
Observable<string | null>> => pipe(map((filter: CarFilter) => filter.name));
...
public readonly
vm$ = vm<PageViewModel>({
name: {
observable: this.queryParams$.pipe(mapToName()), // Using the `mapToName()` operator
initialValue: null,
},
...
});
Demo
Check out the full demo on StackBlitz
Conclusion
- Now we know how to create typesafe view models.
- If we assign types that are not allowed, we get compilation errors.
- We can use the
UnaryFunction
to create custom RxJS operators.
The source code of the @bryanhannes/typed-view-model
NPM package can be found
on Github
Other articles you might like
-
Generating icon components from SVG files with NX and Angular
-
Angular + NGINX + Docker
-
How to Call the OpenAI API Directly from Angular (with streaming)
-
Custom TitleStrategy in Angular
-
RxJS catchError: error handling
-
RxJS distinctUntilChanged: filtering out duplicate emissions
-
RxJS combineLatest: how it works and how you can use it in Angular
-
Delaying streams with RxJS debounceTime
-
Real-life use cases for RxJS SwitchMap in Angular
-
Transforming data with the RxJS Map operator
-
Reactively storing and retrieving URL state in Angular
-
Let's build an Image Generator with OpenAI and Angular
-
Why you should externalize your Angular Configuration