babel-plugin-tcomb icon indicating copy to clipboard operation
babel-plugin-tcomb copied to clipboard

Adding type safety, gradually. Part I

Open gcanti opened this issue 9 years ago • 3 comments

Goal

The goal of this series of posts is to show how you can add type safety, both statically and at runtime, to your untyped codebase gradually and with a gentle migration path.

Static and runtime type checking are complementary and you can get benefits from both.

Tools

I will use the following tools:

  • Runtime type checking
    • tcomb is a library for Node.js and the browser which allows you to check the types of JavaScript values at runtime with a simple and concise syntax. It's great for Domain Driven Design and for adding safety to your internal code.
    • babel-plugin-tcomb is a babel plugin which compiles Flow type annotations to corresponding tcomb models and asserts.
  • Static type checking (optional)
    • Flow is a static type checker for JavaScript.

Why?

Runtime type checking (tcomb)

  • you don't want or you can't use Flow
  • you want refinement types
  • you want to validate the IO boundary (e.g. API payloads)
  • you want to enforce immutability
  • you want to leverage the runtime type introspection provided by tcomb's types

Static type checking (Flow)

babel-plugin-tcomb is Flow compatible, this means that you can run them side by side, statically checking your code with Flow and let tcomb catching the remaining bugs at runtime.

Gentle migration path

You can add type safety to your untyped codebase gradually:

  • first, add type annotations where you think they are most useful, file by file, leveraging the runtime type safety provided by tcomb
  • then, when you feel comfortable, turn on Flow and unleash the power of static type checking
  • third, for even more type safety, define your refinement types and validate the IO boundary

Setup

First, install via npm:

npm install --save tcomb
npm install --save-dev babel-plugin-tcomb

Then, in your babel configuration (usually in your .babelrc file), add (at least) the following plugins:

{
  "plugins" : [
    "syntax-flow",
    "tcomb",
    "transform-flow-strip-types"
  ]
}

If you are using the react preset, the babel-plugin-syntax-flow and babel-plugin-transform-flow-strip-types plugins are already included:

{
  "presets": ["react", "es2015"],
  "passPerPreset": true, // <= important!
  "plugins" : [
    "tcomb"
  ]
}

You can download Flow from here.

Get started

Say you have this untyped function:

function sum(a, b) {
  return a + b;
}

Adding type annotations is easy, just add a colon and a type after each parameter:

// means "both `a` and `b` must be numbers"
function sum(a: number, b: number) {
  return a + b;
}

For a quick reference on type annotations, start here.

Type annotations are not valid JavaScript, but they will be stripped out by babel-plugin-transform-flow-strip-types so your code will run as before.

Now let's introduce intentionally a bug:

function sum(a: number, b: number) {
  return a + b;
}

sum(1, 2);   // => ok
sum(1, 'a'); // => throws Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number
screen shot 2016-06-23 at 11 09 50

Note that you can inspect the stack in order to find where the error was originated. The power of Chrome Dev Tools (or equivalent) are at your disposal.

Runnning Flow

In order to run Flow, just add a .flowconfig file to your project and a comment:

// @flow

at the beginning of the file. Then run flow from you command line. Here's the output:

$> flow
src/index.js:7
  7: sum(1, 'a'); // => throws Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number
     ^^^^^^^^^^^ function call
  7: sum(1, 'a'); // => throws Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number
            ^^^ string. This type is incompatible with
  2: function sum(a: number, b: number) {
                                ^^^^^^ number

Types

You are not limited to primitive types, this is a annotated function which works on every object that owns a name and a surname property:

function getFullName(x: { name: string, surname: string }) {
  return x.name + ' ' + x.surname;
}

getFullName({ name: 'Giulio' }); // => throws Uncaught TypeError: [tcomb] Invalid value undefined supplied to x: {name: String, surname: String}/surname: String

All the Flow type annotations are supported.

Immutability

Immutability is enforced by tcomb at runtime. The values passed "through" a type annotation will be immutables:

function getFullName(x: { name: string, surname: string }) {
  return x.name + ' ' + x.surname;
}

var person = { name: 'Giulio', surname: 'Canti' };
getFullName(person);
person.name = 1; // throws TypeError: Cannot assign to read only property 'name' of object '#<Object>'

Next post

In the next post I'll talk about how to tighten up your types with the help of refinements.

Note. If you are interested in the next posts, watch this repo, I'll open a new issue for each of them when they are ready.

gcanti avatar Jun 23 '16 09:06 gcanti

Dude! Not sure how to say this, but I love you. It's been awesome getting this flow experience into my projects. I've installed the ide-flow atom plugin and I get autocomplete and type information displayed in my IDE now! No words for how sick this is.

You are a thorough legend.

ctrlplusb avatar Jun 23 '16 18:06 ctrlplusb

@ctrlplusb Welcome to @gcanti fan club. You are not the only one! I want to buy this man a beer. 🍺

volkanunsal avatar Jun 23 '16 18:06 volkanunsal

I'm pleased to announce the next post on refinements

gcanti avatar Jul 14 '16 09:07 gcanti