Schema.org Enumerations in TypeScript

Last time, we talked about modeling the Schema.org class hierarchy in TypeScript. We ended up with an elegant, recursive solution that treats any type Thing as a "@type"-discriminated union of ThingLeaf and all the direct sub-classes of the type. The next challenge in the journey of building TypeScript typings for the Schema.org vocabulary is modeling Enumerations.

Learning from Examples

Let’s look at a few examples from the Schema.org website to get a better sense of what Enumerations look like.

First up, I looked at PaymentStatusType, which can take any one of these values: PaymentAutomaticallyApplied, PaymentComplete, PaymentDeclined, PaymentDue, or PaymentPastDue. PaymentStatusType is used in the paymentStatus property on the Invoice class.

Here’s an excerpt from an example of an invoice:

{
    "@context": "http://schema.org/",
    "@type": "Invoice",
    // ...
    "paymentStatus": "http://schema.org/PaymentComplete",
    "referencesOrder": [
      // ...
    ]
}

Here, the value of an Enumeration appears as an absolute IRI.

Looking at other examples, however, such as GamePlayMode which appears in playMode on VideoGame shows up differently:

{
  "@context": "http://schema.org",
  "@type": "VideoGame",
  // ...
  "playMode": "SinglePlayer",
  // ...
}

Here, SinglePlayer shows up as an IRI reference relative to the current "@context". The exact mechanics of "@context" are a bit cumbersome to describe inline here, but I’ll be discussing them in a future post. Suffice to say, in this example, if your context is "http://schema.org" then you can use "SinglePlayer" in contexts where an IRI reference is expected as a relative IRI reference, resolving to "http://schema.org/SinglePlayer".

Therefore, the same enumeration value can be represented by multiple strings that happen to resolve to the same thing. But, for us to know the set of all possible strings we can use to represent an enumeration value, we’ll need to know the @context used. As a matter of fact, this also applies to the "@type" property, so we’ve been assuming a context of http[s]://schema.org all along.

The schema-dts uses a smarter approach: when using the schema-dts-gen CLI tool, the user can specify exactly what their @context should be. Then, the "@type"s produced are rendered as the appropriate relative IRI string literals, if the context allows it, and otherwise an absolute IRI. The pre-packaged schema-dts package, uses "https://schema.org" as its context.

Choosing our Representation

Say the goal of the typings is to easily allow writing new JSON-LD. Then, clarity might be desirable: choose to only allow absolute IRIs or only allow relative IRIs (when permitted). This way, redundant but still valid JSON-LD would be rejected by the type checker, but completions would be much cleaner and all new code would look the same.

On the other hand, say the goal of the typings is to easily validate existing JSON-LD. Then, we probably want it to only emit type errors on invalid string literals. That is, we’ll probably want our typings to include all plausible ways of referencing an enum value, given a @context.

For now, we’ll choose to standardize on just absolute IRIs (like http://schema.org/SinglePlayer) since those are the most common and the clearest.

So far, one might think of representing enums simply:

enum GamePlayMode {
  SinglePlayer = "https://schema.org/SinglePlayer",
  MultiPlayer = "https://schema.org/MultiPlayer",
  CoOp = "https://schema.org/CoOp"
}

or perhaps:

type GamePlayMode =
  "https://schema.org/SinglePlayer" |
  "https://schema.org/MultiPlayer" |
  "https://schema.org/CoOp";

both of these are equally valid, though using const enums in the first approach might more desirable.

Enumerations can have other values…

If only it were that simple. For example, take DeliveryMethod which, in this example is represented as so:

  "deliveryMethod": {
    "@type": "DeliveryMethod",
    "name": "http://purl.org/goodrelations/v1#UPS"
  },

That’s right. DeliveryMethod still extends Enumeration, which is a class type. Enumeration itself extends Intangible which extends Thing.

Our representation will need to change (accounting to the insights learned from the class hierarchy article), then:

const enum GamePlayModeEnum {
  SinglePlayer = "https://schema.org/SinglePlayer",
  MultiPlayer = "https://schema.org/MultiPlayer",
  CoOp = "https://schema.org/CoOp"
}

type GamePlayModeBase = EnumerationBase;  // Doesn't extend other properties

interface GamePlayModeLeaf extends GamePlayModeBase  {
  "@type": "GamePlayMode";
}

type GamePlayMode = GamePlayModeEnum | GamePlayModeLeaf;

Here, we allow for a GamePlayMode to be either the Enum value (represented either as a union of string literals or a const enum) or the Leaf value.

Enums can have sub-classes

The DeliveryMethod example also shows us sub-types are possible. Note that ParcelService, for instance, is a class type (albeit not an interesting one; it adds no properties).

This means we’ll need to include sub-classes in our final type union of the enum, just as we did with Classes in general.

Enums can extend regular classes

A regular class can have sub-classes that themselves are enumerations. Let’s take a look at the PhysicalExam, for an example. PhysicalExam is an enum type that extends a non-enum MedicalProcedure (notice how Schema.org models that as having two parents: MedicalEnumeration and MedicalProcedure).

Using the recursive method defined previously, this means that:

  1. MedicalProcedure is a union of it’s “leaf” class, and all it’s subclasses, which includes “Enums”
  2. PhysicalExam and other enum sub-classes are defined as above: a union of a true Enum type, a leaf type, and all direct sub-classes.

Putting it all Together

Essentially, we can see that Enumerations really are just classes that can also take a special value (an IRI reference). All of the rules of classes we have learned before still apply, but now there’s an addition: our final union contains an XyzEnum type in the union, in addition to XyzLeaf and all sub-classes.

Take a look at the Audience enum for example, and notice it’s very nested nature:

Schema.org Audience has a very nested nature, with 4 direct subclasses, one of which is an enum, with subclasses of it's own.
Schema.org Audience and it’s subclasses. MedicalAudience is an Enum subtype of Audience, with a subtype of it’s own: Patient.

The rules provided so far compose nicely to allow us to represent this complex structure. I’ll leave this as an exercise to the reader to see how the TypeScript structure ends up here, but isn’t too different from the final result you’ll see in schema-dts.

Modeling Schema.org Schema with TypeScript: The Power and Limitations of the TypeScript Type System

Recently, I published schema-dts (npm, GitHub), an open source library that models JSON-LD Schema.org in TypeScript. A big reason I wanted to do this project is because I knew some TypeScript type system features, such as discriminated type unions, powerful type inference, nullability checking, and type intersections, present an opportunity to both model what Schema.org-conformant JSON-LD looks like, while also providing ergonomic completions to the developer.

In a series of posts, I’ll go over some of the Structured Data concepts that lent themselves well to TypeScript’s type system, and those concepts that didn’t. First up: the type hierarchy of JSON-LD Schema.org Schema, and how can be represented in TypeScript.

Note: I’ll be describing JSON-LD in general in very broad strokes and will spend more time discussing how Schema.org JSON-LD looks like in particular. For those who are familiar with the JSON-LD spec, you’ll see I took a few liberties. This is because schema-dts makes a few assumptions, such as the @context being a known constant, etc. schema-dts also foregoes some features, such as specifying multiple types of a node object, etc.

Modeling the Schema.org class structure with the TypeScript Type System

Schema.org JSON-LD node objects are always typed (that is, they have a @type property that points to some IRI–a string–describing it). Given a @type you know all the properties that are defined on a particular object. Object types inherit from each other. For example, Thing in Schema.org has a property called name, and Person is a subclass of Thing that defines additional properties such as birthDate, and inherits all the properties of Thing such as name. Thing has other sub-classes, like Organization, with it’s own properties, like logo.

Let’s use this minimal example to try a few approaches:

1. Modeling each with inheritance

interface Thing {
  "name": string;
}
interface Person extends Thing {
  "@type": "Person";
  "birthDate": string;
}
interface Organization extends
    Thing {
  "@type": "Organization";
  "logo": string;
}

If we had a const something: Thing , then we could assign it to a Thing, Person, or Organization. So that’s a start! But there are a few problems:

  • Using type Thing on it’s own isn’t quite right, as it is missing a @type annotation. More broadly, non-leaf types (types that a super-class of another) are not representible this way.
  • Writing object literals inline will cause TypeScript’s excess property checks to complain that "@type", "birthDate", and "logo" are not a known property of Thing.
  • Lacking completions for "@type". If I was filling in a complex nested object whose property had some type, it would be great if I can look for helpful completions on "@type" and see what allowed types exist for a certain property.

2. Modeling each object individually

interface Thing {
  "@type": "Thing";
  "name": string;
}
interface Person {
  "@type": "Person";
  "name": string;
  "birthDate": string;
}
interface Organization {
  "@type": "Organization";
  "name": string;
  "logo": string;
}

Another approach altogether is to fully roll all parent types of each object separately. This solves the first problem, where types that are a superclass of other types can still be represented, but introduces (and exacerbates) the existing problems.

  • “Sub-classes” are not assignable to their parent types. For example, an object of type Person cannot be assigned to a variable of type Thing. It also cannot be used as the value of a property of a super-class type.

3. Modeling super-classes as discriminated unions

type Thing =
    Person | Organization;
interface Person {
  "@type": "Person";
  "name": string;
  "birthDate": string;
}
interface Organization {
  "@type": "Organization";
  "name": string;
  "logo": string;
}

By defining Thing (or, generally, a parent class) as a union of it’s sub-classes, it’ll behave like a discriminated union. It’s discriminated because each possible type within the union has a property (@type) that is sufficient to tell the compiler which type of the union that object is.

Discriminated unions allow us to achieve assignability, completions (typing ‘"@type": "‘ inside of a Thing will suggest "Person" or "Organization"), and proper type checking without tripping up excess property checking when writing properties of a sub-class.

The problem? We’re back to not being able to individually express a type with sub-classes (e.g. Thing) individually. Back to the drawing board.

4. Hybrid Approach: Modeling parent classes within unions

When we think of the Thing Schema.org class, three separable concepts come to mind. It could be a node

  1. with the actual "@type": "Thing"
  2. that has all the properties of Thing
  3. that has a "@type" equal to that of any of the (direct or indirect) sub-classes of Thing.

(1) can be represented as a specific object literal; (2) can be represented as some type to be extended (or intersected); and (3) can be represented as a discriminated union.

Graph illustration of TypeScript interfaces described in the nest 4 code blocks.
Representing the Type Structure of our set of classes
interface ThingBase {
  "name": string;
}
interface ThingLeaf
    extends ThingBase {
  "@type": "Thing";
}
interface Person
    extends ThingBase {
  "@type": "Person";
  "birthDate": string;
}
interface Organization
    extends ThingBase {
  "@type": "Organization";
  "logo": string;
}
type Thing = ThingLeaf | Person | Organization;

Here, Thing, Person, and Organization can all be used independently. And we have the properties we want:

  • A Person or Organization is assignable to a Thing.
  • A Thing can exist with "@type": "Thing".
  • Thing is a discriminated union and can suggest "Thing", "Person", or "Organization" when typing a "@type" value.

TypeScript will also type check our deeply nested properties according to their types.

Best thing about this approach, it’s recursive. Consider, for example, two sub-types of Organization: Airline and Corporation. Those can be modeled recursively as the graph shows below.

The class hierarchy is composable by making sure "Base" types extend other "Base" types, and union types aggregate all "complete" types, in addition to leaf types.
Examples of the recursive nature of the type model

Getting to a Working System

When schema-dts generates types, it uses a very similar model as described above. Notably different, is that I use type intersections (A & B) instead of interfaces (interface B extends A) to simplify some of the nested expressions that show up.

In schema-dts, every “node” type is represented like Thing in that it has a “base” type, and represents the “real” type as a union of a leaf and other sub-classes.

In future articles, I’ll discuss how to represent “enums”, the limitations of the TypeScript type system with data types, and how to represent properties.

About those Side-effects in Observables, an Angular Use Case

When testing a codebase in Angular Ivy, I ran into a bunch of test failures I wasn’t seeing before. ExpressionChangedAfterItHasBeenCheckedErrors were being thrown around. In debugging these failures, I found that many of them are the results of side-effects in Observables and Observable pipe operations. I happened to describe these earlier in my piece on Observables, Side-effects, and Subscriptions.

Consider this minimal reproduction:

@Component({
  selector: 'widget-editor',
  templateUrl: 'widget_editor.html'
})
export class WidgetEditor {
  constructor(private readonly service: WidgetService) {}

  widgetName = '';
  widgetConfig$ = this.service.getWidget('my_widget').pipe(
    map(widgetDetails => {
      this.widgetName = widgetDetails.name;
      return widgetDetails.config;
    })
  );
}

There’s enough code smell from looking at this alone: an unsuspecting user subscribing to widgetConfig$ will result in member variables of WidgetEditor getting mutated. That’s strange behavior, but with the right Angular template, it can also become an outright bug:

<h1>Editing {{widgetName}}...</h1>
<ng-container *ngIf="(widgetConfig$ | async) as widget; else loading">
  <h2>Widget Config<h2>
  <widget-config-view [widget]="widget"></widget-config-view>
</ng-container>
<ng-template #loading>Loading...</ng-template>

Testing this code might give you an error that looks something like:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ''. Current value: 'my_widget_name'.

Why?

The normal case

At render time, widgetConfig$ has never been subscribed to. Therefore, no HTTP request has been made (Observables are lazy that way), and the map operator therefore hasn’t been called. The value of widgetName is ''. When Ivy tries to render the <ng-container>, the Async Pipe triggers subscribing to widgetConfig$, and, eventually, all the side-effects that come with it’s operators.

When widgetConfig$ is subscribed to, using the Async Pipe, it kicks off the Observable (and, eventually, all of it’s side-effects).

In most cases, we get the widget result asynchronously. Side-effects happen at some point, and change detection forces a re-render cycle that picks up the new widgetName and widgetConfig$ results.

The error case

What if a test injected a FakeWidgetService instead, whose getWidget call used the of RxJS operator to immediately return a value? The default scheduler used in the of operator immediately returns the value within the operator when subscribed. This means that subsequent pipes are executed immediately, and, in our case, the side-effects run immediately.

Consider that use case and follow along with the initial render: widgetName is initially rendered the first time as the empty string ''. Later, when widgetConfig$ is subscribed to by the Async Pipe, it immediately changes the value of widgetName. By the time rendering is complete, Angular notices that the value of widgetName it just rendered is different than the current value.

Can’t we just change the test scheduler?

We could, and that would fix this problem. But then you’re sort of leaking abstractions; asking services you depend on to have Observables with schedulers that behave a certain way, and depending on more than just the implied contract of the interfaces you use.

The Fix

@Component({
  selector: 'widget-editor',
  templateUrl: 'widget_editor.html'
})
export class WidgetEditor {
  constructor(private readonly service: WidgetService) {}

  private readonly widget$ = this.service.getWidget('my_widget').pipe(
    publishReplay(1), refCount()
  );

  widgetName$ = this.widget$.pipe(
    map(widgetDetails => widgetDetails.name)
  );

  widgetConfig$ = this.widget$.pipe(
    map(widgetDetails => widgetDetails.config)
  );
}

A common reason for side-effects (as is with this case) is to reuse an intermediate value within a sequence of transformations elsewhere. Making that explicit, by exposing the intermediate piece as a private Observable itself, will clean things up quite a bit.

To make sure that a subscription to widgetName$ and widgetConfig$ wouldn’t result in two separate HTTP requests (or underlying side-effects, whatever those may be), making sue the widget$ Observable is shared between the two will be important.

Observables, Side-effects, and Subscriptions

My previous articles on using AsyncPipe and data refresh patterns in Angular hint at some common anti-patterns dealing with Observables. If there’s any common thread in my advice, it is: delay unpacking an Observable into its scalar types when performing logic you can rewrite as side-effect-free, leaving code with side-effects for subscription callbacks and other downstream logic.

My two earlier articles focused on cases users can benefit from handling more of the object’s lifecycle in its Observable form. In other words, cases where the Observable was being subscribed to and unpacked too soon. Instead, I suggested transforming the Observable using operators like map, switchMap, filter, etc. and taking advantage of the power offered by this form. In the case of Angular, it provides AsyncPipe, which takes the care of the step with side-effects (actually rendering the page) in template code.

There are some exceptions to this line of thinking, namely do and tap are reactive operators exclusively there for functions with side effects. I’ll leave a discussion of right vs less right reasons to use do/tap for a later article. But I’ll mention logging, error reporting, and caching of otherwise pure functions as one valid use of side-effects.

This article uses RxJS in code examples, but applies to broader reactive concepts.

Let’s explore a few of these cases:

1. Displaying data represented by Observables

Say I have two Observables wrapping some object in a storage format (e.g. JSON), and I’d like to display it.

Unpacking an observable too soon

let customerName: string;
let customerBalance: number;

nameObservable.subscribe(name => {
  customerName = name;
  if (customerName && customerBalance) {
    processAndDraw();
  }
});
balanceObservable.subscribe(balance => {
  customerBalancer = balance;
  if (customerName && customerBalance) {
    processAndDraw();
  }
});
function processAndDraw() {
  alert(`${customerName}: $${customerBalance.toFixed(2) USD`);
}

If a caller unpacks an observable too soon, it means they’re dealing with scalars, passing things around by global state. Developers might have trouble handling changes, such as adding a third data source to show.

Unpacking an Observable too late

combineLatest(nameObservable, balanceObservable).pipe(
  map(([name, balance]) => {
    alert(`${name}: $${balance.toFixed(2) USD`);
  })
).subscribe();

On the one hand, this is much shorter and more expressive! This is effectively maps Observable<[string, number]> into an Observable<void> which happens to perform side effects when subscribed to. The subscriber, however, has no idea what action will take place from just looking at a type or signature. Even with the code snippet above used as-is, it is very easy to forget about that last .subscribe() call, which–given that Observables are lazy by default and only perform useful actions when subscribed to–renders this whole snippet a no-op.

One final reason side-effects are bad in operators: that these side-effects can be performed an arbitrary number of times per event based on how many distinct subscribers are listening to an Observable.

A better trade-off

combineLatest(nameObservable, balanceObservable).pipe(
  map(([name, balance]) =>
    `${name}: $${balance.toFixed(2) USD`
  )
).subscribe(text => alert('Text'));

2. Avoiding Unnecessary Indirection through Subjects

In some ReactiveX implementation, a Subject is a powerful concept that allows an event publisher to share events with subscribers, as an Observable. It is also quite overused. Dave Sexton wrote a great piece in 2013 about whether or not to use a Subject, and further quoted Eric Meijer’s reasoning for disliking them:

[Subjects] are the “mutable variables” of the Rx world and in most cases you do not need them.

Erik Meijer, via To Use Subject or Not To Use Subject?

In particular, I’ve come across many examples in the wild violating Sexton’s first piece of advice, “What is the source of the notifications?” Here’s an egregious anti-pattern:

class DogNewsProvider {
  constructor(news: Observable<News>) {
    news.subscribe(newsItem => {
      if (newsItem.category === "Dog") {
        this._dogNews.next(new DogNews(newsItem));
      }
    });
  }
  private readonly _dogNews = new ReplaySubject<DogNews>(1);

  get(): Observable<DogNews> {
    return this._dogNews.asObservable();
  }
}

Here, we’re providing Observable<DogNews>, based on source data contained by another observable. In between, however, we’re routing information from an Observable to a ReplaySubject, which we are manually triggering on each event from the source observable.

This has a few problems:

  1. The code above is flawed in that the news observable provided to DogNewsProvider is never unsubscribed to. Modifying the class to support unsubscribing is easy, but not ergonomic and easy to miss.
  2. A bunch of indirection is happening between the source and the output, making the flow of data less clear.
  3. The advantage of a replay Subject (namely that someone subscribing to an Observable will get some number of events the missed–1 in this example) can be replicated by applying the shareReplay operator to the source observable.

A better approach

class DogNewsProvider {
  constructor(news: Observable<News>) {
    this.dogNews = news.pipe(
      filter(newsItem => newsItem.category === "Dog"),
      map(newsItem => new DogNews(newsItem)),
      shareReplay(1),
    );
  }
  readonly dogNews: Observable<DogNews>;
}

A subscription to dogNews is not leaked in this case, and events will only fire while there are active listeners to this Observable. Further, the flow from news all the way to dogNews is clearly and directly explained by looking at the code, without jumping between callbacks.

One way to think about this indirection is in the way Sexton described: there’s some unnecessary indirection in unpacking an Observable, just to pack it into an Observable again and send it off.

Another way to think about this indirection is to look at the subscribe() method. Does it have side-effects? Yes, it does. It calls a method with side effects on a variable outside its scope. Can it be rewritten in a way that doesn’t? It can. There’s no incremental state we’re trying to maintain on purpose (other than the replay operator semantics); no I/O in the subscription; the side-effects outside of our scope that we’re modifying is mainly to pipe data from one place to another. These provide clues that there might be a better way to rewrite our callback into a series of monadic operators on an Observable.

3. Subscribing when switchMap or flatMap would do

Seeing nested subscribe callbacks is a good sign some logic can be reworked. Let’s take a simplified example:

function getBalance(
    name: string,
    showBalance: (balance: number) => void) {
  backend.getAccountIdByName(name).subscribe(id => {
    backend.getBalanceById(id).subscribe(balance => {
      showBalance(balance);
    });
  });
}

Other than the same issues with unsubscriptions as above, the callback hell also obscures what would be a relatively simple data flow:

function getBalance(
    name: string,
    showBalance: (balance: number) => void) {
  backend.getAccountIdByName(name).pipe(
    switchMap(id => backend.getBalanceById(id)
  ).subscribe(balance => {
    showBalance(balance);
  });
}

Keep showBalance in a subscription, and transform an account holder name all the way to an account balance through operators on Observables.

The bigger advantage to organizing code like this is that it promotes opportunities to refactor:

Use opportunities to factor out Observable-returning functions

We can actually factor out part of getBalance that actually returns an Observable<number>. This allows it to be reused, combined, and multiplexed with other Observables and Observable operators as needed. Observables are also a uniform, ubiquitous API that is cancelable, retryable, etc.

function getBalance(name: string) {
  backend.getAccountIdByName(name).pipe(
    switchMap(id => backend.getBalanceById(id)
  );
}

Observables are a uniform, ubiquitous API that is cancelable, retryable, etc.

Summary

An Observable going through a series of transformation operators from source to final result is:

  1. Cancelable through-and-through; cancelling a subscription to a resultant Observable will cancel any underlying subscriptions opened to that end.
  2. Composable in its own right; and
  3. A ubiquitous immutable API that gives callers flexibility in manipulating return values.

I propose side-effects being a great first-order heuristic as far as what can reasonably be kept within a composed Observable. When needed, operators like do and tap will sometimes make sense.

Data and Page Content Refresh patterns in Angular

Part of why I recommend using RxJS Observables all the way through in Angular TypeScript code, and only unpacking them at the closest point to where the UI is declared (often using the | async pipe), is because it makes other transformations on an Observable available and convenient. Two such examples include retry and refresh logic.

Two common reasons to reload/refresh data being displayed by a component include:

  1. A user action in the application causes the data to change (especially, if it does so in ways that might result in complex state changes, etc.), and/or
  2. The data being displayed can change over time (e.g. due to a progression/change of it’s state in the backend, or by another user, etc.)

Let’s start with a simple example:

@Component({
  selector: 'task',
  template: `<ng-container *ngIf="(task$ | async) as task">
    <h1>{{task.name}}</h1>
    <p>Status: {{task.status}}</p>
    <sub-task *ngFor="let subTask of task.subtasks" [subTask]="subTask"/>`
})
export class TaskComponent {
  constructor(private readonly http: HttpClient) {}
  readonly task$ = this.http.get('/api/tasks/foo');
}

Suppose the user adds a ‘Mark as Complete’ button, that mutates the server-side state of all sub-tasks. How do obtain the latest authoritative data from the server about the state of our side? Here’s an approach:

export class TaskComponent {
  constructor(private readonly http: HttpClient) {}
  
  private readonly refreshToken$ = new BehaviorSubject(undefined);
  private readonly task$ = this.refreshToken$.pipe(
    switchMap(() => this.http.get('/api/tasks/foo')));

  markAsComplete() {
    this.http.post('/api/tasks/foo', { state: State.Done })
      // N.B. contrary to my advice elsewhere, I'm happy to
      // directly subscribe here because this subscribe
      // callback has side effects.
      // Further, I don't worry about unsubscribing since
      // this returned Observable is a one-shot observable
      // that will complete after a single request.
      .subscribe(() => this.refreshToken$.next(undefined));
  }
}

Adding refresh logic this way will minimally affect our template code and looks relatively clean. Better yet, adding additional mutating functions simply need to call refreshToken$.next to make sure new data is loaded.

What about regularly polling for updates? This can be implemented simply as well:

export class TaskComponent {
  constructor(private readonly http: HttpClient) {}
  
  private readonly autoRefresh$ =
    interval(TASK_REFRESH_INTERVAL_MS)
      .pipe(startWith(0));

  private readonly refreshToken$ = new BehaviorSubject(undefined);

  private readonly task$ =
    // Notice that combineLatest will only trigger the first
    // time when an event triggers on all input Observables
    // you are combining.
    // BehaviorSubject always triggers its latest value when
    // you subscribe to it, so we're good there.
    // An interval() Observable will need a 'startWith' to
    // give you an initial event.
    combineLatest(this.autoRefresh$, this.refreshToken$)
    .pipe(
      switchMap(() => this.http.get('/api/tasks/foo'))
    );

  markAsComplete() {
    this.http.post('/api/tasks/foo', { state: State.Done })
      .subscribe(() => this.refreshToken$.next(undefined));
  }
}

What if we didn’t want to hardcode foo as the task we look up? Well, Angular’s ActivatedRoute already uses Observables. Rather than using route.snapshot.params['task_id'] or similar, we can use the actual Observable results and get our minds off manually refreshing that data:

export class TaskComponent {
  constructor(private readonly http: HttpClient,
              private readonly route: ActivatedRoute) {}
  
  private readonly autoRefresh$ =
    interval(TASK_REFRESH_INTERVAL_MS)
      .pipe(startWith(0));

  private readonly refreshToken$ = new BehaviorSubject(undefined);

  private readonly task$ =
    combineLatest(this.route.params,
                  this.autoRefresh$,
                  this.refreshToken$)
    .pipe(
      switchMap(([params]) =>
        this.http.get(`/api/tasks/${params['task_id']}`))
    );

  markAsComplete() {
    this.route.params.pipe(
      map(([params]) => params['task_id']),
      switchMap(taskId => 
        this.http.post(`/api/tasks/${taskId}`, { state: State.Done })
      ))
    .subscribe(() => this.refreshToken$.next(undefined));
  }
}

As a monad, an Observable is a neat and tidy functional construct. You can transform it using a rich set of operators. In RxJS, those also include catchError for error handling and retrying, timed events, and combinations of multiple monads into a monad of multiple items. With the view of Observables as just another monad, reactive programming becomes just a simple extension on top of functional programming.

Dealing with these Observables for as much of the data lifecycle as possible means that you can take advantage of these constructs to transform immutable data using neat operators, rather than dealing with unpacking this data into mutable scalars.

%d bloggers like this: