An Overview of Type Safety in Javascript

Posted March 31, 2018 by Carl Reyes in Development

Like a lot of developers who learned to code 10+ years ago, I got started learning C++ and Java. The public static void main and int sum (int a, int b) syntax was intimidating but I was determined to power through it so I could make the next great video game. After struggling with endless compiler errors I discovered Ruby and my eyes were opened; why would I ever write types again?!

After writing productive code for a few years (but never my awesome zoneless-Everquest clone sadly) I got sick of the constant runtime errors and got on board with writing automated tests. Test-driven Ruby/Rails is the perfect combination of developer happiness and good, durable code and that's why we develop that way here at Vaporware.

That brings us to today where I've recently started on a new client project. The core of the app is handling large amounts of external data and processing it in real-time. We built a prototype in Ruby but decided to start exploring other languages to see what kind of a performance gain we could get so I started learning Go. The performance gains were incredible but the real thing I learned was how much I enjoyed static typing. I like how the compiler told me what was broken when I messed stuff up, how readable and predictable the code was (I always know what a function is supposed to return) and how much it forced me to think about the code that I was writing. I really wanted my next project to be in a type safe language - and then I found out my next project was in Javascript.

This post is not about why type safety is awesome. I'm assuming you're already on board. These are just notes from my research on how to implement types in my next JS project. If you need to be convinced:

PropTypes

The project I'm working on is an internal tool to streamline the onboarding experience for our new clients. It's going to be a highly interactive single-page app and our go-to library for that at Vaporware is React. Reactjs already provides some type safety with their components via propTypes. It's incredibly straightfoward and easy to use.

import React from 'react'
import PropTypes from 'prop-types'

const Greeter = ({firstName, lastName}) => ( <h1>Hello {firstName} {lastName}!!</h1> )

Greeter.propTypes = { firstName: PropTypes.string.isRequired, lastName: PropTypes.string.isRequired, }

export default Greeter

While it does provide some type safety and it's easy to use, it has it's limitations. It's really only built for component props. If your code has a lot of state or a lot of functions then propTypes probably won't be enough. propTypes also errors at runtime and not before hand so it's probably too late to be useful anyway.

The React team has acknowledged these shortcomings and for decently sized codebases, recommends using Flow or Typescript instead of propTypes.

Flow

Flow is a library developed by Facebook that is a static type checker looks for errors in your Javascript code. Flow doesn't consider itself a separate language but a tool on top of Javascript. This gives the benefit of being able to use normal .js files and also gradually incorporating it into different parts of your app (by adding the //@flow annotation to a file).

A basic example in React looks very similar to propTypes


import * as React from 'react'

type Props = { firstName: string, lastName: string, }

const Greeter = (props: Props) => ( <h1>Hello {props.firstName} {props.lastName}!!</h1> )

export default Greeter

The power of Flow is that you can define types or use predefined types in other parts of your app, besides just component props. Let's look at a typical counter component example from the Flow docs.

// @flow
import * as React from 'react'

type State = { count: number, }

class Counter extends React.Component<{}, State> { state = { count: 0, }

handleClick = (event: SyntheticEvent<HTMLButtonElement>) => { (event.currentTarget: HTMLButtonElement)

<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setState</span><span class="token punctuation">(</span><span class="token parameter">prevState</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span>
  count<span class="token operator">:</span> prevState<span class="token punctuation">.</span>count <span class="token operator">+</span> <span class="token number">1</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span>

}

render() { const { count } = this.state; return ( <div> <p>Count: {count}</p> <button onClick={this.handleClick}> Increment </button> </div> ) } }

export default Counter

In this example we get to enforce the type that our components State should be and also the type and target type of what our handleClick function should expect. This is incredibly powerful and will provide pre-runtime error checking of our stateful components. Flow also has the benefit of being built/maintained by the same company that build React so the integration is awesome (as demonstrated by the SyntheticEvent type that Flow provides).

While I haven't looked into it that much, Flow can also be used on the server. There's not specific about React that Flow needs to do be able to do static type checking.

// @flow
function fullname(firstName: string, lastName: string): string {
  return firstName + " " + lastName
}

Calling the above with console.log(fullName("Carl", 28); would result in a nice type error.

Flow does have it's downsides. If Flow comes across a library it doesn't have type definitions for it generates definitions with loose types like the any type. When using functions from those libraries or passing data to/from them your code might still compile with errors because any really does mean that anything will compile correctly. This coupled with the fact that the Flow community isn't nearly as large as the TypeScript community means you lose a lot of the benefits of Flow when using unsupported libraries.

TypeScript

TypeScript is a statically typed superset of Javascript written and maintained by Microsoft. While not technically a different language, one of TypeScripts differences from Flows is that it is written with a different .ts file extension and compiles down to regular Javascript. I actually discovered TypeScript first when working on a client project in Node and it feels like a traditional statically-typed language.

TypeScripts big focus is on interfaces that check the shape that values may have. You'll usually see this concept references as duck typing or structural subtyping. Interfaces can also be exported and used around your app so you're not defining the same types multiple times (I believe you can do this in Flow as well).

import * as React from 'react';

export interface Person { firstName: string; lastName: string; }

export const Greeter = (props: Person) => ( <h1>Hello {props.firstName} {props.lastName}!!</h1> )

TypeScript also has massive community support. The any typed package imports that are a problem with Flow don't relaly exist in TypeScript. The DefinitelyTyped, which is currently sitting at ~15k stars on Github, is a large collection of type declaration files for most common libraries out there. They're easily added to a project with yarn add --dev @types/node and whenever you import/use a node package you'll have all the type safety you'd have if you'd have written it yourself.

TypeScript also has the concept of immutability with the readonly keyword. Re-writing the Person example like following:

export interface Person {
  readonly firstName: string;
  readonly lastName: string;
}

makes it so you cannot change those values. Very handy when writing functional components and provides extra assurance that your functions/components will return as you'd expect.

Conclusion

In practice, TypeScript and Flow are very similar and either would be an upgrade over plain Javascript. As someone who likes to write functional Javascript and likes community support, I'd probably go with TypeScript right now if I had to choose. The DefnitelyTyped library is a huge benefit to the TypeScript community and provides plenty of examples to get started defining your types.

Next time we'll talk about the last two options I explored in this static typing deep dive: Elm and ReasonML.

Feel free to reach out to us on Twitter @vpware if you have any questions or if you want Vaporware to get started on building your next app!

Vaporware

Related Insights