---
title: JavaScript Type Checkers are More than Linters
teaser: 'Treating your JavaScript type checker as a linter doesn''t allow you to fully
  leverage your static type system.

  '
tags: javascript,types
author: Sid Raval
published_on: 2019-11-21
---

I've recently worked with several teams that have sizeable JavaScript codebases.
In order to increase the maintainability of their codebases, they've chosen to
start gradually typing their codebases, using [Flow](https://flow.org), a static
type checker for JavaScript. As a [believer] in the virtues of statically typed
code, I've encouraged this adoption.

If developers on the team do not have experience with statically typed
languages, I've seen the following workflow arise:

- Developers write their JavaScript code as they always have
- They add some types to their codebase
- They run Flow, and fix problems as necessary

This is what I mean by "treating your type checker as a linter". Flow is being
used as an after-the-fact coherence check on code that was written without types
in mind.

## A First Attempt

To give an example of this workflow, let's say we need to produce an object of
the following structure, to pass off to some analytics code.

```javascript
{
  name: "Analytics event name here",
  description: "Human readable description of the event",
  linkURL: "The URL of the link we're reporting analytics for",
  currentPageURL: "The URL of the page the event is happening on"
}
```

To that end, we could write something like the following:

```javascript
const analyticsEvents = {
  onClick: {
    name: "onClick",
    description: "Link click"
  },
  onHover: {
    name: "onHover",
    description: "Link hover"
  }
};

function buildAnalyticsObject(eventName, metadata) {
  const analyticsData = analyticsEvents[eventName];

  if (!analyticsData) {
    return null;
  }

  return {
    ...analyticsData,
    ...metadata
  };
}
```

Since we've been asked to use Flow (and avoid the `any` type as much as
possible), we'll add some types to our function. We're expecting `eventName` to
be a `string`; `metadata` is a caller-supplied `Object`; and we know the
shape of the structure we're returning, so we might write the following:

```javascript
function buildAnalyticsObject(
  eventName: string,
  metadata: Object
): {
  name: string,
  description: string,
  currentPageUrl: string,
  linkUrl: string
} | null {
  const analyticsData = analyticsEvents[eventName];

  if (!analyticsData) {
    return null;
  }

  return {
    ...analyticsData,
    ...metadata
  };
}
```

Flow is [happy] with this level of typing in our code - so, what's the problem?

- Callers of our function need to understand its implementation

  It's not clear that the only valid strings one should call it with are
  `"onClick"` and `"onHover"`. It's also not clear that the `metadata` object
  should not have `name` or `description` properties to prevent shadowing.

- The return type of our function is partially a lie

  Nothing in our code ensures the return type will have each of the properties
  listed. It may not contain the stated properties at all; it's possible that it
  contains only `name` and `description`.

- The optional return type is unfortunate, and asks the caller to handle failure

  It would be great if we could guarantee that we're returning an object
  with the properties, rather than an optional value that the caller has to
  handle.

We can fix all of these issues by thinking about our types _before_ we write
our code.

## A Second Attempt

We'll begin by defining a few of our types up-front. We know we want to handle
two types of events, and we know what structure the result should have:

```javascript
type OnClickEvent = {
  name: "onClick",
  description: "Link click"
};

type OnHoverEvent = {
  name: "onHover",
  description: "Link hover"
};

type AnalyticsEvent = OnClickEvent | OnHoverEvent;

type AnalyticsMetadata = {
  linkUrl: string,
  currentPageUrl: string
};

type AnalyticsWithMetadata = AnalyticsEvent & AnalyticsMetadata;
```

We define a few events, then use the [union operator] \(`|`\) to express that an
`AnalyticsEvent` is an `OnClickEvent` _or_ a `OnHoverEvent`. We then use the
[intersection operator] \(`&`\) to express that our result type should have all
the properties of an `AnalyticsEvent` _and_ an `AnalyticsMetadata`. With the
types defined, we can then implement our function as follows:

```javascript
function buildAnalyticsObject(
  eventName: "onClick" | "onHover",
  metadata: AnalyticsMetadata
): AnalyticsWithMetadata {
  const event = eventNameToEvent(eventName);

  return {
    ...event,
    linkUrl: metadata.linkUrl,
    currentPageUrl: metadata.currentPageUrl
  };
}

function eventNameToEvent(
  eventName: "onClick" | "onHover"
): AnalyticsEvent {
  switch (eventName) {
    case "onClick":
      return {
        name: "onClick",
        description: "Link click"
      };
    case "onHover":
      return {
        name: "onHover",
        description: "Link hover"
      };
  }

  throw new Error(`Unexpected analytics event name: ${eventName}`);
}
```

What have we gained by writing code like this, in terms of the types we defined
up-front?

- Callers of our function no longer need to understand its implementation

  Flow will ensure the `eventName` argument is one of the two valid strings;
  it will also ensure that the `metadata` object passed in contains the two
  necessary properties, `linkURL` and `currentPageURL`. Furthermore, shadowing
  of the `name` and `description` properties is no longer possible. If we try
  to spread the `metadata` object as before, Flow will complain about the
  potential shadowing.

- Our return type tells the whole story

  Callers know the return type of our function is an object with the four
  properties we defined above; they also do not have to handle an optional
  return value.

- We've encoded our domain (analytics work) into the type system

  By defining our types up-front, we're giving future developers information
  about what we knew when implementing the code: the types communicate that we
  only support two events, and that we're only giving two pieces of metadata
  to our analytics.

## Conclusion

The type-driven approach shown above produces a more robust and expressive
solution to the problem we set out to solve. I hope it illustrates the concrete
benefits to leveraging your type system before you write implementations, rather
than after.

[believer]: https://thoughtbot.com/blog/static-types-in-medias-res
[happy]: https://repl.it/@sidraval/FrontMushyNotifications
[union operator]: https://flow.org/en/docs/types/unions/
[intersection operator]: https://flow.org/en/docs/types/intersections/
