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

Cast to opaque type #6957

Closed
jmarceli opened this issue Oct 2, 2018 · 9 comments
Closed

Cast to opaque type #6957

jmarceli opened this issue Oct 2, 2018 · 9 comments

Comments

@jmarceli
Copy link

jmarceli commented Oct 2, 2018

Hi, I would like to use custom opaque type build on top of number type, but working with opaque types seems to be problematic.
Unfortunately, it is impossible to reproduce that on http://flow.org/try because it requires more than one file (one which exports opaque type and other which imports it).

Here is my problem:

Exported types e.g. index.js.flow

export opaque type People: number = number;

Imported types e.g. index.js

import type { People } from './index';

const x: People = 10; // error
const x: People = (10: People); // error
const x = (10: People); // error (WHY THIS IS NOT POSSIBLE?)
const x: People = (10: any); // this is OK, but verbose a little

const x = ((parseInt('123', 10): any): People); // this is also OK, but looks terrible

// more problematic are function calls e.g.
function countPeople(a: People, b: People) {
  return a + b;
}
countPeople(10, 123); // error
countPeople(((10: any): People), ((123: any): People)); // THIS IS TERRIBLE TO READ and WRITE
countPeople((10: any), (123: any)); // ok, but doesn't help much in type checking

Is there any casting method which I'm not aware of?

Message in error cases is similar to this:

Cannot cast 10 to People because number [1] is incompatible with People [2].

@wchargin
Copy link
Contributor

wchargin commented Oct 2, 2018

You can reproduce this in Flow Try using declare opaque type.

I think that you may be misunderstanding the purpose of opaque types.
The entire point of opaque types is that (10: People) is supposed to
be a type error. All your examples boil down to this core fact.

Consider the following example. We can have a module nonneg.js that
defines a type of non-negative numbers. By taking advantage of opaque
types, we can ensure that every value of type NonNegative really is
not negative. Like this:

// @flow
export opaque type NonNegative: number = number;

export function asNonNegative(x: number): NonNegative {
  if (x < 0) throw new Error("negative: " + x);
  return x;
}

export function add(x: NonNegative, y: NonNegative): NonNegative {
  return x + y;
}

From another module, it is not valid to write (-10: NonNegative). The
module can interact with the opaque type only through the public API:

import { asNonNegative, add, type NonNegative } from "./nonneg";
const one: NonNegative = asNonNegative(1);
const two: NonNegative = add(one, one);

Because NonNegative is bounded as a subtype of number, it is legal
to write

const works: number = one - two;

because one and two can be upcast to number and then subtracted.
But note that the result of this subtraction is a number, not a
NonNegative, so the following is not valid:

const fails: NonNegative = one - two;  // fails to typecheck (good!)

If we could just write (10: Person) or (-1: NonNegative), we would
be defeating the purpose of opaque types: the nonneg module would not
be able to provide any encapsulation.

Again, you can reproduce this example in a Flow Try.

@jmarceli
Copy link
Author

jmarceli commented Oct 2, 2018

Many thanks for such a descriptive answer. I was looking for a way to define a millisecond timestamp type in my application to avoid providing other timestamp types (e.g. microseconds) for functions that expect exactly a millisecond timestamp.

Is it a valid use case for opaque types? If not how, is there any other construction to achieve that.

Here is an example: Flow Try - opaque types

@wchargin
Copy link
Contributor

wchargin commented Oct 2, 2018

I was looking for a way to define a millisecond timestamp type in
my application to avoid providing other timestamp types (e.g.
microseconds) for functions that expect exactly a millisecond
timestamp.

Opaque types are one good solution here, yes.

I suggest defining the following simple module:

// @flow
export opaque type MillisecondsTimestamp = number;

export function fromMilliseconds(milliseconds: number): MillisecondsTimestamp {
  return milliseconds;
}

export function toMilliseconds(timestamp: MillisecondsTimestamp): number {
  return timestamp;
}

Then, you can write client code like the following:

// @flow
import {
  type MillisecondsTimestamp,
  fromMilliseconds,
  toMilliseconds,
} from "./millisecondsTimestamp";

function now(): MillisecondsTimestamp {
  return fromMilliseconds(Date.now());
}

function isCloseToNow(ts: MillisecondsTimestamp) {
  return toMilliseconds(ts) > toMilliseconds(now()) - 1000 * 60;
}

(Flow Try link to example here.)

The key thing to note is that in the client code, all conversions are
explicit. You can still write fromMilliseconds(usTimestamp), but in
the process of writing that you should realize that there is a data
mismatch.

Does this make sense?

@wchargin
Copy link
Contributor

wchargin commented Oct 2, 2018

Depending on your constraints, another reasonable solution would be to
encode the units in the values themselves:

type TimeUnit = "SECONDS" | "MILLISECONDS" | "MICROSECONDS";
type Duration = {|+unit: TimeUnit, +scalar: number|};

If you control the implementations of all the functions that you’re
interested in, then this could be a good solution. But if you have to
interface with third-party code that expects raw numbers in particular
units, then this might be too cumbersome.

@jmarceli
Copy link
Author

jmarceli commented Oct 2, 2018

This is exactly the answer I was looking for. Many thanks for that.

@jmarceli jmarceli closed this as completed Oct 2, 2018
@wchargin
Copy link
Contributor

wchargin commented Oct 2, 2018

Great; glad to have helped.

@jmarceli
Copy link
Author

jmarceli commented Oct 3, 2018

Unfortunately, opaque types seem to be not supported by flow-runtime gajus/flow-runtime#209 which is used by babel-plugin-flow-runtime.

@wchargin
Copy link
Contributor

wchargin commented Oct 3, 2018

It sounds like flow-runtime is broken and must be fixed. :-)

(Opaque types are a pretty critical feature. It’s not heartening that
they promise “full Flow compatibility” when they’ve left this issue
unacknowledged and unattended for over three months.)

@gajus
Copy link

gajus commented Jul 22, 2019

(Opaque types are a pretty critical feature. It’s not heartening that
they promise “full Flow compatibility” when they’ve left this issue
unacknowledged and unattended for over three months.)

I have recently took over development of flow-runtime project. Will prioritise fixing this. If there is any interest to contribute a PR, will happily streamline it.

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

No branches or pull requests

3 participants