babel-plugin-tcomb
babel-plugin-tcomb copied to clipboard
Adding type safety, gradually. Part I
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
Flowtype annotations to correspondingtcombmodels 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
Flowand 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
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.
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 Welcome to @gcanti fan club. You are not the only one! I want to buy this man a beer. 🍺
I'm pleased to announce the next post on refinements