Adapted from “Supercharged Types” (http://rtpg.co/2016/07/20/supercharged-types.html) by @rtpg_
Flow has Row Polymorphism, an important feature if you want to encode invariants into types. For people with Haskell experience, Eff is kinda like IO, things with side effects end up in Eff (PureScript users should feel comfortable).
Here’s a tiny example. I’m not going to explain too much about the type mechanisms themselves, just a taste of what is possible.
type DB = { type: 'DB' };type User = {
username: string,
uid: number
};function createUser(username: string): Eff<{ write: DB }, User> {
…
}function lookupUser(username: string): Eff<{ read: DB }, ?User> {
…
}
- the type DB represents a side effect
- the type of createUser is (username: string) => Eff<{ write: DB }, User>. It means that createUser writes to the DB and gives you a User back
- the type of lookupUser is similar: (username: string) => Eff<{ read: DB }, ?User>. Given a string (in our case a username), it will return an action that will read from the DB and return a User (if found).
And here’s a function that will create a user and then look it up
const createThenLookupUser = username => createUser(username)
.chain(user => lookupUser(user.username))
What’s the type of createThenLookupUser? Let’s ask Flow!
$> flow suggest index.jsconst createThenLookupUser = username => createUser(username).chain(user: {uid: number, username: string} => lookupUser(user.uid): Eff<{read: {type: ‘DB’}}, ?{uid: number, username: string}>): Eff<{write: {type: ‘DB’}} & {read: {type: ‘DB’}}, ?{uid: number, username: string}>
Flow is pretty verbose but if you skip the cruft you can see
(username: string) => Eff<{ write: DB, read: DB }, ?User>
The type inference figured out that createThenLookupUser
- writes to the DB (write: DB)
- reads from the DB (read: DB)
Row polymorphism here is used to encode effects. We can combine actions within Eff, and the type system will accumulate the effects and keep track of them.
Whitelisting side effects
Let’s see how you can whitelist the effects returned by a function. I’m going to write a model for a (server side) router.
First some types
type Method = 'GET' | 'POST';// web request
type Req = {
body: string,
header: string,
method: Method
};// web response
type Res = {
body: string,
status: number
};
Second, let’s write an endpoint to register to our service
function signupPage(req: Req): Eff<{ write: DB, read: DB }, Res> {
const username = req.header
return lookupUser(username).chain(user => {
if (user) {
return new Eff(() => ({
body : 'A user with this username already exists!',
status : 400
}))
}
return createUser(username).map(() => ({
body : 'Created User with name ' + username,
status: 200
}))
})
}
Now let’s write the router. A Route is either a GET or a POST endpoint. We want to enforce that GET endpoints can’t write to the db
type GetRoute = {
type: ‘GetRoute’,
path: string,
handler: (req: Req) => Eff<{ read: DB }, Res>
};type PostRoute = {
type: ‘PostRoute’,
path: string,
handler: (req: Req) => Eff<{ read: DB, write: DB }, Res>
};type Route = GetRoute | PostRoute;
Finally our main routes
const routes: Array<Route> = [
{ type: 'GetRoute', path: '/signup', handler: signupPage }
]
But if you run Flow you get the following error
{ type: 'GetRoute', path: '/signup', handler: signupPage }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This type is incompatible with union: GetRoute | PostRouteproperty `write`. Property not found inhandler: (req: Req) => Eff<{ read: DB }, Res>
^^^^^^^^^^^^ object type
This is great. signupPage totally writes to the DB! And GETs should not be allowed to change the server state.
Changing the route definition to
const routes: Array<Route> = [
{ type: 'PostRoute', path: '/signup', handler: signupPage }
]
solves the problem: the endpoint is now allowed to write to the DB, because of the types of PostRoute and signupPage.