Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phantom types #1056

Closed
paulyoung opened this issue Nov 8, 2015 · 9 comments
Closed

Phantom types #1056

paulyoung opened this issue Nov 8, 2015 · 9 comments

Comments

@paulyoung
Copy link

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 type Foo``.

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!

@gabelevi
Copy link
Contributor

gabelevi commented Nov 8, 2015

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

@samwgoldman
Copy link
Member

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

@paulyoung
Copy link
Author

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.

@mhagmajer
Copy link

+1

@JenniferWang
Copy link

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

@simprince
Copy link

+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.

@mhagmajer
Copy link

mhagmajer commented May 22, 2017 via email

@ckknight
Copy link
Contributor

ckknight commented May 23, 2017

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.

@jbrown215
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants