Flow: Support nominal type aliases

Created on 22 May 2015  Â·  25Comments  Â·  Source: facebook/flow

Sometimes I have two types, and both of them are numbers, for instance, but at the same time have absolutely different meaning. Can I express this in Flow? I tried type aliases, but they are simply interpreted as the types they _alias_, so this code type checks:

/* @flow */

type UserId = number;
type NumberOfFollowers = number;

function hasLotOfFollowers(followers: NumberOfFollowers): boolean {
  return followers > 1000;
}

var userId: UserId = 100;

// Expecting error here
hasLotOfFollowers(userId);
$ flow
No errors!
feature request

Most helpful comment

Flow's type system is structural by design and what you are proposing would behave more like a nominal type system. I don't believe the existing behavior of type aliases will change, but we might consider supporting a way to make nominal types, because this absolutely would provide type safety.

However, there are a few details to sort out. Assume we had something like the following syntax:

type UserId = $Nominal<number>

How would you create an inhabitant of the UserId type? Would number be able to flow into a UserId?

I am reminded of Haskell's newtype wrappers. You need to "construct" the value in the newtype and "unwrap" the newtype to get at the inner value, but the compiler is able to erase that at compile time, making it a zero-cost abstraction.

Ignoring the compiler support for unwrapping for a moment, we currently support something like this:

/* @flow */

class UserID {
  id: number;
  constructor(id: number) {
    this.id = id;
  }
  get() {
    return this.id;
  }
}

class AdminID {
  id: number;
  constructor(id: number) {
    this.id = id;
  }
  get() {
    return this.id
  }
}

var userID: UserID = new AdminID(1);
var adminID: AdminID = new UserID(2);
test.js|23 col 26 error|  AdminID
|| This type is incompatible with
test.js|3 col 7 error|  UserID
|| 
test.js|24 col 28 error|  UserID
|| This type is incompatible with
test.js|13 col 7 error|  AdminID

That is to say, classes are nominal in Flow.

I think it would be VERY interesting to support a newtype semantics for classes like this, which wrap a single value, but that would involve rewriting javascript, which flow doesn't actually do—babel does it for us.

Ultimately, I don't have an answer here. Can you write your code to use a class wrapper for these values?

All 25 comments

Flow's type system is structural by design and what you are proposing would behave more like a nominal type system. I don't believe the existing behavior of type aliases will change, but we might consider supporting a way to make nominal types, because this absolutely would provide type safety.

However, there are a few details to sort out. Assume we had something like the following syntax:

type UserId = $Nominal<number>

How would you create an inhabitant of the UserId type? Would number be able to flow into a UserId?

I am reminded of Haskell's newtype wrappers. You need to "construct" the value in the newtype and "unwrap" the newtype to get at the inner value, but the compiler is able to erase that at compile time, making it a zero-cost abstraction.

Ignoring the compiler support for unwrapping for a moment, we currently support something like this:

/* @flow */

class UserID {
  id: number;
  constructor(id: number) {
    this.id = id;
  }
  get() {
    return this.id;
  }
}

class AdminID {
  id: number;
  constructor(id: number) {
    this.id = id;
  }
  get() {
    return this.id
  }
}

var userID: UserID = new AdminID(1);
var adminID: AdminID = new UserID(2);
test.js|23 col 26 error|  AdminID
|| This type is incompatible with
test.js|3 col 7 error|  UserID
|| 
test.js|24 col 28 error|  UserID
|| This type is incompatible with
test.js|13 col 7 error|  AdminID

That is to say, classes are nominal in Flow.

I think it would be VERY interesting to support a newtype semantics for classes like this, which wrap a single value, but that would involve rewriting javascript, which flow doesn't actually do—babel does it for us.

Ultimately, I don't have an answer here. Can you write your code to use a class wrapper for these values?

Thank you for the detailed answer! It would be indeed very interesting if Flow would support something like nominal types. This would provide support for extra type safety for those, who want it, but I understand now that the cost will be to annotate every creation of an inhabitant.

I was considering using classes as wrappers, but in my particular case it would be an overkill. Anyway this technique might be useful in some cases.

Thanks!

Yeah, thanks @samwgoldman for the detailed answer. We have thought about newtype off and on, and don't quite know how to do it other than inventing wrapper syntax that gets stripped off.

So something like

newtype userid = number // or type userid = $Nominal<number> as you have above

that satisfies:

var u: userid = userid(0);
(u: number); // OK, we can explicitly require unwrapping to be stricter, but this should suffice

Basically this design allows userid to be used wherever numbers can, e.g. in arithmetic operations. But not all numbers are userids. Also, not all userids are ages, if age is another newtype: the wrapping has to be explicit.

Want to work on something like this?

Stripping off whatever wrapper syntax is the big hurdle here, IMHO. Adding functionality to flow that transforms source files makes me hesitate to volunteer for this one.

Maybe we could leverage the casting syntax both ways?

newtype userid = number; // agree this is good "official" syntax, but I would start with $Nominal
var u = (0 : userid);
u * 10; // error
(u : number) * 10; // ok

Then, instead of using the vanilla flow logic for type casts, run something with special cases for type aliases before falling back to vanilla flow.

@samwgoldman allowing casting for this anywhere is problematic

  • you are now taking _any_ newtype of a number in your multiplication function
  • there is no restriction on where wrapping and unwrapping can occur

In Haskell one controls all this at the definition site, in particular whether a constructor is exported and/or a function for unwrapping is exported. By convention the unwrapping function usually has an un prefix, so unUserId.

newtype UserId = UserId { unUserId :: Int } deriving (Read, Show, Eq, Ord, Num)

If I only want to provide the ability to unwrap, but not to construct, I just export unUserId.
Now by not exporting the newtype constructor I can guarantee that the rest of my functions are properly working with minutes.

So to use casting as is, one needs the ability to do fine-grained exports. However, the other problem is the ambiguity of over-loading casting. It is no longer clear whether the user wants to cast or deal with a newtype. So you might consider adding a new syntax such as (0 : number -> userid) and (u : userid -> number). Note that I am not using the fat arrow which is already used for function types.

@gregwebs Good insight.

Our constraint is that we can only play with the type language, not the runtime language. Seems like you're keyed into that, based on the syntax you recommended. I think that syntax could work (although I worry about confusion when a user types -> isntead of =>, as I sometimes do).

Regardless, your point about newtypes being one-to-one is well taken.

+1 to this overall request. This is one of the first things I look for in a type system.

To contribute to the bike shed UX discussion of the arrow, I propose "~>". Or you can consider avoiding an arrow: just "~"; or "castto"; or "x from y"; or reversing the order so the arrow points backwards to be really even more distinguished from "=>", are other brainstorms. :-)

You can achieve the desired effect with this trick (not sure if this is mentioned elsewhere?):

// @flow


type UserId = { _Type_UserId_: void }  // dummy field unlikely to happen accidentally in code


function UNSAFE_CAST(x:any) { return x; }
var NewUserId : (val:string)=>UserId = UNSAFE_CAST;
var FromUserId : (userId:UserId)=>string = UNSAFE_CAST;


var userId : UserId = NewUserId('u1');

// At runtime, just a naked string
console.log("Runtime value:", userId, "Runtime type:", typeof userId);

// This line would fail the flow type check
// var str : string = userId;

var str : string = FromUserId(userId);

This is pretty close to a zero-cost abstraction, except for calls to the no-op identity functions for "boxing" and "unboxing". However, one option here (if using something like webpack) could be to write a loader to strip out these conversion functions.

Or one could just use the any type to box/unbox for free... e.g.

var userId2 : UserId = ('u1':any);
var str : string = (userId:any);

The second line could be made a little less distasteful

// assert that it was a UserId before unsafely casting it to string
var str : string = ((userId:UserId):any);

@latos using classes is better for this purpose, because they are actually nominally typed

@latos I agree with @vkurchatkin since you must wrap / unwrap anyway.

Your trick might be used to define custom refinements though

type Integer = number & { __refinement_Integer: void };

function integer(n: number): ?Integer {
  return n % 1 === 0 ? ((n: any): Integer) : null
}

function foo(n: number) {
  return n.toFixed(2)
}
function bar(n: Integer) {
  return n.toFixed(2) // <= no need to unwrap, `Integer`s are `number`s
}

integer(1.1) // => null
const i = integer(1)

foo(1)
// bar(1) // error

if (typeof i === 'number') {
  foo(i) // <= again, `Integer`s are `number`s
  bar(i)
}

@vkurchatkin the aim of the exercise is to implement nominal types with zero overhead. Using a class has the overhead of boxing the primitive in an object, which is what this trick avoids.

@gcanti the refined type thing you did is pretty cool. Not sure what you mean by "must we wrap/unwrap anyway"? the runtime type is the raw primitive. The NewUserId/FromUserId functions would probably get optimised out because they do nothing... but they are optional anyway, as you can just use the type casts instead as shown later in my example. It works the same as your example (no need to unwrap at runtime), except it adds the compile time requirement to wrap/unwrap, for stronger type safety, which I think is what @rpominov was after. E.g. a "UserId" type that is entirely incompatible with "string" at compile time in either direction. A refined type is incompatible in one direction only, which is also useful for some contexts (e.g. when we think of "integer" as a subtype of "number").

@latos no, it doesn't. It's the same thing: you use any to cast primitive to a class type

@latos with unwrap I mean that you must do something (a cast or pass through FromUserId) in order to get a string, otherwise while the runtime type is the raw primitive, Flow will still complain. Example:

function foo(x: UserId) {
  return x.length // <= here Flow complains: property `length`. Property not found in object type
}

so you must unwrap

function foo(x: UserId) {
  return ((x: any): string).length
}

and you must do that _everytime_ you use a UserId value in your code. I agree that there's no runtime overhead but seems pretty awkward. On the other hand using classes adds a small runtime penalty but it's way more clean and future proof.

going hacky IMO refinements capture better the spirit of the original question of @rpominov (i.e. UserId !== NumberOfFollowers as types), plus they don't add the requirement to always unwrap in order to use them

type UserId = number & { __refinement_UserId: void };
type NumberOfFollowers = number & { __refinement_NumberOfFollowers: void };

function userId(n: number): UserId {
  // dummy refinement, predicate always true
  return ((n: any): UserId) // <= wrap only once
}

function hasLotOfFollowers(followers: NumberOfFollowers): boolean {
  return followers > 1000 // here I can use `>` without unwrapping
}

var userId = userId(100) // or even ((100: any): UserId)

hasLotOfFollowers(userId) // <= error!

Yes, UserId and NumberOfFollowers are still numbers, but I think is a feature in this context.

@vkurchatkin I think I see what you mean. You're saying, do the same trick, but use a class instead of the hack with the unlikely field? (I thought you meant to just use a class without any "unsafe" casting). I agree a class is a better idea :)

@gcanti Yes, I consider the fact that you have to unwrap it to be a feature. Depending on context, it may or may not be what someone wants. For instance, if all I want is to declare I want a UserId, and no one can accidentally pass me a plain string (or a SomethingElseId) then doing the refinement trick is sufficient, and retains the convenience of using it as a string directly.

However, I might also want the additional strength of having complete incompatibility with a string, because I don't want it accidentally concatenated/rendered in an inappropriate fashion. A few real-life examples would be:

  • a physical unit, e.g. "distance" that might be internally represented in metres, but you don't want it to accidentally get rendered directly. you always want it to be explicitly rendered to a string in the current "units" constant (e.g. metric or imperial). In an engineering tool, missing a spot & printing the wrong units could be catastrophic
  • a "password" or "credit card" details value that you don't want to be logged or output accidentally, you would normally convert it to a string via a sanitising helper
  • similarly, an "unsafe raw html" type, etc.
    Of course, in the above examples, not all may necessarily need the "zero cost abstraction" - you could just actually box them at runtime.

@latos Good point. So the pattern would be:

helper

function unsafeCoerce<A, B>(a: A): B {
  return ((a: any): B)
}

newtype definition

class Inches {}
const of: (a: number) => Inches = unsafeCoerce
const extract: (a: Inches) => number = unsafeCoerce

newtype utilities

function show(x: Inches): string {
  return `Inches(${extract(x)})`
}

function lift(f: (a: number) => number): (a: Inches) => Inches {
  return (a) => of(f(extract(a)))
}

function lift2(f: (a: number, b: number) => number): (a: Inches, b: Inches) => Inches {
  return (a, b) => of(f(extract(a), extract(b)))
}

Example:

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

const a = of(2)
const b = of(3)

const sumInches = lift2(sum)

log(a) // error: Inches. This type is incompatible with number
sumInches(1, 2) // error: number. This type is incompatible with Inches
show(sumInches(a, b)) // => "Inches(5)"

It should be added to the documentation until it is not imlemented.
This https://medium.com/@gcanti/phantom-types-with-flow-828aff73232b#.5w33oy8hc is very similar.
These compile time only refinements are very useful in a lot o ways eg. domain-driven-design
number and string refinements, and class/object meta-annotation (like empty interfaces in java)

Instead of $Nominal, might it be appropriate to use the existing $Subtype for this?

going back to the original example:

type UserId = $Subtype<number>;
type NumberOfFollowers = $Subtype<number>;

As far as I can tell, this currently does nothing (it's the same as making them both type number). However - maybe I'm misunderstanding what is meant by subtype, but it seems to me this is supposed to say: every UserId is a number, and every NumberOfFollowers is a number, but an unqualified number cannot be assumed to be one of those.

Without adding new syntax, these types could be used with the foo: UserID = (3: any) trick.

If there is new syntax added, I've always wished there were a succinct way to tell flow what I know about a type that it can't infer in a particular case. Something like foo = (3: !UserID) as shorthand for the "upcast to any, then downcast to the specified type."

That's a nice pattern @gcanti! Flow exposes type params to static class methods, so "newtype" can be a self-contained parent class:

class Newtype<Outer, Inner> {
  constructor: (_: { __DONT_CALL_ME__: void }) => void;
  static to(x: Outer): Inner { return (x: any); }
  static from(x: Inner): Outer { return (x: any); }
}
class Foo extends Newtype<Foo, number> {}

const foo: Foo = Foo.from(42);
Math.abs(foo); // flow error
Math.abs(Foo.to(foo))

@mkscrg: Nice!

I had a go at shrinking this a little (removing the redundant type / Outer parameter) by using the this type:

class Newtype<Inner> {
  constructor(_: empty): void {}
  static to(x: this): Inner { return (x: any); }
  static from(x: Inner): this { return (x: any); }
}
class Foo extends Newtype<number> {}

const foo: Foo = Foo.from(42);
Math.abs(foo); // Flow error
Math.abs(Foo.to(foo))

Demo here in the REPL.

(Note: This only works from 0.37.0 onwards; previous versions allowed stuff like Foo.to(Foo.to(42)), Foo.from(Bar.to(42)) and other nonsense through. It'll "work", but allow things through it shouldn't.)

Follow-up edit: I've been reticent to use the "Newtype" terminology, mostly because newtype lets you do things that we can't with this, eg. newtype Foo a = Foo (a -> String), where it's not a straight "wrapping". I'm using the term TypeWrapper in my own code.

Follow-up edit #2: I've put this up as a library here: https://www.npmjs.com/package/flow-classy-type-wrapper … with a blog-post explainer.

generally since i'd like to use it for numbers for e.g. physics stuff, i'd like it to be as close to a zero runtime overhead cost abstraction as possible :-) Or is it such a small runtime overhead that I really shouldn't mind?

@damncabbage thanks for the concise demo! I am guessing from the thread above that using 'class' still does have some runtime overhead?

@raould:
When the Flow types are stripped out, you're still left with two things:

To be clear, the "class" in this is just a handy container for defining the type, the "from" and the "to" functions all in one go. You're not creating new wrapped objects from this class; you're just calling the statically-defined functions as a way for Flow keep track of type conversions, with the tiny(?) run-time cost of passing in a value that is immediately returned.

As a bonus: I've previously done some checking with V8 that saw it optimise away these no-op calls when they're called enough times (eg. in a hot loop), but I unfortunately don't have the test setup working anymore to post logs from.

So yeah; I personally wouldn't worry about the overhead. Regardless, I can't think of any way of making this faster while preserving the (what I think are important) explicit to/from casting semantics that the above examples have.

I hope that helps. 😄

@damncabbage Thank you for this nice solution—I've been diving into Haskell and really missed newtypes in Flow.

After some playing around I realised that if you are willing to sacrifice the beauty of the code it's possible to completely avoid any overhead by using comment syntax:

/* flow-include
class Newtype<Inner> {
  constructor(_: empty): void {}
  static to(x: this): Inner { return (x: any); }
  static from(x: Inner): this { return (x: any); }
}
*/
/* flow-include
class Foo extends Newtype<number> {}
*/
const foo: Foo = /*::Foo.from(*/42/*::)*/;
Math.abs(foo); // Flow error
Math.abs(/*::Foo.to(*/foo/*::)*/)

Besides being ugly, this also might not syntax highlight / intellisens nicely with the editors not supporting comment syntax, but other then that, this is true 0-overhead newtype.

Opaque types landed in master a couple of days ago: https://github.com/facebook/flow/commit/68dd89ecdf1ddcdca7fe04ca24ad7c0731e4c7e7 🎉

Sample use from https://twitter.com/vkurchatkin/status/886385324422836224 (from whom I found about this):

Closing since opaque types went out in 0.51
docs here: https://flow.org/en/docs/types/opaque-types/

Was this page helpful?
0 / 5 - 0 ratings

Related issues

john-gold picture john-gold  Â·  3Comments

cubika picture cubika  Â·  3Comments

marcelbeumer picture marcelbeumer  Â·  3Comments

jamiebuilds picture jamiebuilds  Â·  3Comments

bennoleslie picture bennoleslie  Â·  3Comments