Opaque type does not unify with underlying type in declaring module when used as type parameter
- 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
Assessment
This issue has not been assessed yet.