Flow: Phantom types

Created on 8 Nov 2015  Â·  9Comments  Â·  Source: facebook/flow

I was talking to @jeffmo on IRC about this and was asked to create an issue.

I have a project where I'm using some types purely as type constraints, or phantom types. This is currently available at https://github.com/paulyoung/tss.

In order to make sure these types can't be constructed I must do the following:

class _Foo {}; export type Foo = _Foo;

This way, even if someone does import { Foo } from Bar instead of import type { Foo } from Baz, new Foo() causes the error Constructor cannot be called on typeFoo``.

However, this has the undesired result of the error for an incorrect type to read This type is incompatible with _Foo, rather than when simply doing export class Foo {} which yields This type is incompatible with Foo.

I'm also unsure how to create a declaration file that achieves the same behavior (or even if this is necessary, but it seemed to be the only way to use one flow-annotated project inside another).

I'd prefer some way of exporting an anonymous type, perhaps something like this:

export type Foo = class {};
export type Bar = class {};

Of course, this would require that Foo and Bar are considered to be different types.

If there are any other ways to achieve this currently, I'd love to know how.

Thanks!

feature request

Most helpful comment

I think the missing feature is opaque types. Currently, type aliases are transparent. If you have

export type URIString= string;

Then anyone who imports URIString can use it interchangeably with string. This is great if you're using type aliases to avoid rewriting some complicated types, but it's not so great if you want to create new types that should be checked nominally and not structurally.

Hack does this with the newtype keyword. Their opaque types are transparent within the file they're declared, which is nice for writing type constructors and eliminators. Though maybe we could do something clever with exports instead. Like what if you could write

// URIString.js
type URIString = string;
export function encode(s: string): URIString {
  return encodeURI(s);
}
export function decode(s: URIString): string {
  return decodeURI(s);
}
export opaque type { URIString };

// Other module
import { encode, decode } from './URIString';
var encoded_string = encode('Hi there'); // No error
var decoded_string = decode(encoded_string); // No error

decide("Other string"); // Error: string ~> URIString
encode(encoded_string); // Error: URIString ~> string

If we had that, then you could create your phantom types as opaque. Then they would be checked nominally and the errors would reference them therefore by name.

Though now that I've written this, there's another feature which I'm not sure how it would fit in. With opaque types, it's nice sometimes to be able to specify a subtyping behavior. Like we might like to say that URIString is a subtype of string (so all URIString's are string's, but not all string's are URIString's). This isn't really what you need, but it's useful. Hack has support for this too. So maybe it would be better to follow their syntax, which would mean something like

// URIString.js
newtype URIString as string = string;
export function encode(s: string): URIString {
  return encodeURI(s);
}
export function decode(s: URIString): string {
  return decodeURI(s);
}
export type { URIString };

// Other module
import { encode, decode } from './URIString';
var encoded_string = encode('Hi there'); // No error
var decoded_string = decode(encoded_string); // No error

decide("Other string"); // Error: string ~> URIString
encode(encoded_string); // No error, since URIString is a subtype of string

All 9 comments

I think the missing feature is opaque types. Currently, type aliases are transparent. If you have

export type URIString= string;

Then anyone who imports URIString can use it interchangeably with string. This is great if you're using type aliases to avoid rewriting some complicated types, but it's not so great if you want to create new types that should be checked nominally and not structurally.

Hack does this with the newtype keyword. Their opaque types are transparent within the file they're declared, which is nice for writing type constructors and eliminators. Though maybe we could do something clever with exports instead. Like what if you could write

// URIString.js
type URIString = string;
export function encode(s: string): URIString {
  return encodeURI(s);
}
export function decode(s: URIString): string {
  return decodeURI(s);
}
export opaque type { URIString };

// Other module
import { encode, decode } from './URIString';
var encoded_string = encode('Hi there'); // No error
var decoded_string = decode(encoded_string); // No error

decide("Other string"); // Error: string ~> URIString
encode(encoded_string); // Error: URIString ~> string

If we had that, then you could create your phantom types as opaque. Then they would be checked nominally and the errors would reference them therefore by name.

Though now that I've written this, there's another feature which I'm not sure how it would fit in. With opaque types, it's nice sometimes to be able to specify a subtyping behavior. Like we might like to say that URIString is a subtype of string (so all URIString's are string's, but not all string's are URIString's). This isn't really what you need, but it's useful. Hack has support for this too. So maybe it would be better to follow their syntax, which would mean something like

// URIString.js
newtype URIString as string = string;
export function encode(s: string): URIString {
  return encodeURI(s);
}
export function decode(s: URIString): string {
  return decodeURI(s);
}
export type { URIString };

// Other module
import { encode, decode } from './URIString';
var encoded_string = encode('Hi there'); // No error
var decoded_string = decode(encoded_string); // No error

decide("Other string"); // Error: string ~> URIString
encode(encoded_string); // No error, since URIString is a subtype of string

See some earlier discussion in #465. I recently hacked together an interface file for testcheck that uses classes to simulate an opaque type.

I believe I've stumbled upon a nicer way to do this while working around a limitation with the current support for export.

// @flow
// Properties.js

export class Margin {}
export class Padding {}
// @flow
// index.js

// export * from "./Properties";
import * as Properties from "./Properties";
export type Margin = Properties.Margin;
export type Padding = Properties.Padding;

Using this approach new Margin() and new Padding() isn't valid, and Margin is incompatible with Padding as a type constraint.

+1

I really want this feature to distinguish Int and Float on client side.

+1 this is important to differentiate Int from Float. This avoids human errors where we forget to round float to int value and cause errors down the road.

Also, it's something that Hack for PHP already supports. I hope this comes
around for JS as well soon...

On Mon, May 22, 2017, 20:11 simprince notifications@github.com wrote:

+1 this is important to differentiate Int from Float. This avoids human
errors where we forget to round float to int value and cause errors down
the road.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/facebook/flow/issues/1056#issuecomment-303177599, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ACmsItVJ1pzsYfa_eUzxNBjo8oJ3-gsEks5r8c-7gaJpZM4GePRJ
.

Perhaps a syntax like

type URIString = $Opaque<string>;

would be useful before adding concrete keywords.

This falls in line with other private types such as $Shape or $Abstract.

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