09 April 2023 ~ 8 min read

Building a TypeScript assertion function


Building a TypeScript assertion function

In my projects, I make heavy use of ComponentStore for state.

When declaring the shape of my state for my ComponentStore classes, I often have pieces of state that are initially optional, but eventually need to have a value. For example, let's say I have a form which allows my user to edit a user record fetched from a backend API.

My ComponentStore class might look like this:

type User = {
    id: string;
    name: string;
    password: string
};

type UserStoreState = {
    isProgress: boolean;
    user?: User;
};

@Injectable()
export class UserEditStore extends ComponentStore<UserStoreState> {
  private http = inject(HttpClient);

  readonly user$ = this.select(({ user }) => user);

  constructor() {
    super({
      isProgress: false
    })
  }

  ...
}

In the above example, I've created a UserEditStore which extends ComponentStore and manages its state, declared in the UserStoreState type. The state declares just two properties - inProgress which is either true or false, and user which is the instance of the user being edited. This is declared with the ? syntax, specifying that the state property is optional - it doesn't exist when the ComponentStore is instantiated, but will be loaded from our backend API at some point in the life of the application.

To round out our ComponentStore definition, I've added an inject of the HttpClient service provided by Angular, and a selector for the user state property.

Loading the user

Fleshing out the code for loading the user from our backend would typically use an effect, something like this:

readonly loadUser = this.effect((userId$: Observable<string>) =>
  userId$.pipe(
    switchMap((userId) => {
      this.patchState({ isProgress: true});

      return this.http.get<User>(`/api/users/${userId}`).pipe(
        tapResponse(
          (result) => {
            this.patchState({ user: result });
          },
          (err) => {
            // TODO: Handle error
          },
        ),
        finalize(() => this.patchState({ isProgress: false }))
      );
    }),
  )
);

The effect takes a user's ID and calls our backend API to return a User object. Assuming the API request succeeds, the state is patched with the result, meaning that our state now has a value for the user property.

Saving changes

When the user wishes to save their changes, we would post the current value of the edited User back to the server using our API. We use the withLatestFrom operator to get the current value of the user property off the state, exposed through the user$ Observable. Let's implement that:

readonly saveUser = this.effect((trigger$) =>
  trigger$.pipe(
    withLatestFrom(this.user$),
    switchMap(([, user]) => {
      this.patchState({ isProgress: true });
      return this.http.post(`/api/users/${user.id}`, user).pipe(
        tapResponse(
          () => {
            // TODO: Show a message that the user was saved.
          },
          (err) => {
            // TODO: Handle error
          },
        ),
        finalize(() => this.patchState({ isProgress: false })),
      )
    })
  )
);

But... when we type this code, we see that TypeScript identifies a problem:

Error

TypeScript complains that we're trying to access the id property off the user object, which is possibly undefined.

How can we solve this?

One simple way is to use the optional chaining operator, and prefix the id property access with a ?:

return this.http.post(`/api/users/${user?.id}`, user).pipe(

Well, that solves the problem, doesn't it? Not so fast!

While we've resolved the compilation problem, what if user really is undefined? What will we be posting back to our backend? We've introduced a logical bug in our application.

When we call saveUser(), user should be defined!

It is a bug in our application if saveUser can be called without a user having been loaded.

Solving this the right way

So, if it's a bug in our application, we should complain loudly.

My initial way of doing this was to throw an error:

switchMap(([, user]) => {
  if (!user) {
    throw new Error('No user is set');
  }

  this.patchState({ isProgress: true });

  return this.http.post(`/api/users/${user.id}`, user).pipe(

Now we're checking whether user has a value and throwing an Error if it doesn't.

TypeScript is clever enough to recognise that we tested whether user had a value and would throw an Error if it didn't. Therefore by the time we access user.id in the http.post call, user is guaranteed to be defined.

Can we do better?

This is all well and good, but I found that peppering my effects with if statements and throwing Error for each piece of state I needed to check was becoming tiresome, not to mention cluttering up my code with a lot of boilerplate.

This let me down the path of trying to write a function that would check whether a parameter is was passed was defined, and throw an Error if it wasn't. Coming from other languages where assert functions were common, I thought I'd try something like that.

Typically an assert function will take a parameter and check whether it is defined and throw an Error if it's not. That was exactly what I wanted.

So, I started out with something like this:

export const assertDefined(value: unknown) {
  if (!value) {
    throw new Error('Value is not defined!');
  }
}

and, I used it like this:

readonly saveUser = this.effect((trigger$) =>
  trigger$.pipe(
    withLatestFrom(this.user$),
    switchMap(([, user]) => {
      assertDefined(user);

      this.patchState({ isProgress: true });
      return this.http.post(`/api/users/${user.id}`, user).pipe(

        tapResponse(
          () => {
            // TODO: Show a message that the user was saved.
          },
          (err) => {
            // TODO: Handle error
          },
        ),
        finalize(() => this.patchState({ isProgress: false })),
      )
    })
  )
);

I thought that seemed pretty simple. However, when I tried to use this, TypeScript still told me that user was 'possibly undefined'.

Error 2

Oh! Of course, how can TypeScript know that the function I called actually throws an error if the parameter is was passed isn't defined? It's not a mind-reader!

Luckily, TypeScript was way ahead of me, and already provided a specific syntax for building assertion functions.

The general form is to declare a generic function, with a type parameter, and then specify its "return type" as asserts. That way, TypeScript knows that it can assume code following the function call has checked the assertion.

So, this was the next iteration of my assertDefined function:

export const assertDefined = <T>(value: T | undefined | null)
  : asserts value is T => {
  if (!value) {
    throw new Error('Value is not defined');
  }
}

In the above code I'm declaring a function which takes a value of type T, which can also be undefined or null. The return value of the function states that the function asserts that value is of type T.

This all seemed reasonable, but then I tried to use it, and TypeScript gave me this pretty cryptic error message:

Error 3

"Assertions require every name in the call target to be declared with an explicit type annotation."

What on earth does that mean?

OK, so off to Google I went, which lead me a useful StackOverflow question and answer.

The upshot of it is that TypeScript can't use arrow functions (=>) for assertions. (Actually, it can. See further down...)

So, it was a simple matter of converting my arrow function into a traditional function declaration:

export function assertDefined<T>(value: T | undefined | null)
  : asserts value is T {
  if (!value) {
    throw new Error('Value is not defined');
  }
}

That satisfied TypeScript and my code then complied with my custom assertDefined call.

Final refinements

As it stood the assertDefined function was a little problematic, in that it didn't account for "falsey" values.

Every seasoned JS/TS developers will know about the quirks of falsey values - boolean false, or empty strings, or zero are interpreted as falsey. And while I'm usually going to be calling my function to test object variables, for completeness it made sense to handle other potentially falsey values.

So, I modified my function to account for those. Along the way, I also decided it would be nice to have an optional error message to throw.

Here's my final version:

export function assertDefined<T>(value: T | undefined | null, error?: string)
  : asserts value is T {
  if (typeof value === 'number' || typeof value === 'boolean') {
    return;
  }

  if (!value) {
    throw new Error(error || 'Value is not defined');
  }
}

In this version, I introduced an optional error parameter, which allowed specifying what message to throw.

Next, I introduced checks for the typeof the value being number or boolean to just return.

Finally, the value is checked for whether it's falsy. If it is, the Error is thrown with either the custom error message or the default 'Value is not defined'.

Actually...

Thanks to some feedback from Jason Warner, it turns out we can use arrow functions for assertions. We just have to declare the type of the function signature. This becomes a little more verbose, but we can now define it like this:

export const assertDefined: <T>(value: T | undefined | null, error?: string)
  => asserts value is T
  = <T>(  value: T | undefined | null,  error?: string)  => {
  if (typeof value === 'number' || typeof value === 'boolean') {
    return;
  }

  if (!value) {
    throw new Error(error || 'Value is not defined');
  }
};

Cleaner code

I've used this technique in my current project, and it's cleaned up the code in complex effects to make it more readable. My preference is to not use optional state values, but where they're unavoidable, this technique helps a lot.

Finally, using arrow functions instead of a traditional function definitions is my preference, so it's nice to see this syntax is also supported.


Headshot of Craig Shearer

Hi, I'm Craig. I'm a full-stack software architect and developer based in Auckland, Aotearoa/New Zealand. You can follow me on Twitter