facebook / facebook/flow

Opaque type does not unify with underlying type in declaring module when used as type parameter

Open
#6,421 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Rust
Stars
22.3k
Forks
1.9k
PR merge metrics
No merged PRs in 30d

Description

Opaque type does not unify with underlying type in declaring module when used as type parameter

I am running into what I believe is a bug relating to instantation of
polymorphic types at an opaque type. I have a workaround, but I want to
make sure that I am understanding the situation correctly, and in
particular that this workaround is not expected to be made inviable in a
future release.

The example requires two files, and does not reproduce in the
playground (but does on a fresh install of Flow 0.73.0, etc.).

In `id.js`:

```js
// @flow

export type Id = (T) => T;

export function makeNumberId(): Id {
return (n: number): number => n;
}
```

In `nonneg.js`:

```js
// @flow

import {type Id, makeNumberId} from "./id";

export opaque type NonNeg: number = number;

export const nonNegId_broken: Id = makeNumberId(); // error

export const numberId: Id = makeNumberId();
export const nonNegId_working: Id = numberId;
```

The error, on the line thusly marked, is:

> Cannot assign `makeNumberId()` to `nonNegId_broken` because `number`
> is incompatible with `NonNeg` in type argument `T`.

Full error text

```
$ yarn -s flow --message-width 80
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ nonneg.js:7:44

Cannot assign makeNumberId() to nonNegId_broken because number [1] is
incompatible with NonNeg [2] in type argument T [3].

nonneg.js
4│
5│ export opaque type NonNeg: number = number;
6│
[2] 7│ export const nonNegId_broken: Id = makeNumberId(); // error
8│
9│ export const numberId: Id = makeNumberId();
10│ export const nonNegId_working: Id = numberId;

id.js
[3] 3│ export type Id = (T) => T;
4│
[1] 5│ export function makeNumberId(): Id {
```

(Note that [the embedding of this code into the playground][1] does not
reproduce the error, which is not that surprising given the context.)

[1]: https://flow.org/try/#0PTAEFpNBLATA6AVgZwpAULApgYwDYCGATlqAC4CeADqQJKwA8AKgHygC8oAFEwJQdsmAbky5CJUADMArgDscZaAHtZoALYEA1lgBy0tQCMsRel14AuUPQaz9RoixHoQacKFkrZWAOZJUkcHQsAA8qJSIyUCUqAgBHaVJKGlAdFR0fS1tDYw53O2MnF1TIggVpAjw8ClADIiVtVWhVMgALUipCCm86uQQg0PDInBVkSI9ZdO96AH1a+qxZS2tUiZ82Tg1tPWyTWDMnELCI0GHZUbyd+iXGLPt19S1dfN39-qOhkbG0nxmAd3DNE1vNcGCtJvdbsZ6EIgA

First, I posit that the two blocks of declarations should be equivalent.

Second, I posit that the error is incorrect; that is, the second block
of declarations is completely valid. The result of `makeNumberId` is an
`Id`. Within the `nonneg.js` module, `number` is known to be
exactly equal to `NonNeg`, not just a subtype or supertype of it.
Therefore, `Id` and `Id` should be completely
equivalent, regardless of the fact that `Id` is neither covariant nor
contravariant in its argument.

It is of course not true that for every type constructor `F` it actually
makes sense that values of `F` and `F` are equivalent:
for instance, `subtract: (number) => number` violates `NonNeg`
invariants. But, critically, within the module that declares `NonNeg`,
this substitution should be valid, because it is up to this module to
maintain the invariants.

It would be great if someone could confirm my understanding of the
situation. I’d like to write code that depends on this behavior, so I
don’t want to upgrade Flow only to find that my code no longer
typechecks for nontrivial reasons. I would _really_ like to not have
casts through `any` here.

Thanks!

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.