let f = v => {
let x = ref(#b)
x := v
}
let g = () => {
let x = #a
switch x {
| #a => f(x)
}
}

but the equivalent code using React.useState does not (ReScript Playground):

let f = v => {
let (_x, setX) = React.useState(() => #b)
setX(_ => v)
}
let g = () => {
let (x, _setX) = React.useState(() => #a)
switch x {
| #a => f(x) /* This has type: [#a]
But this function argument is expecting: [> #b]
The first variant type does not allow tag(s) #b */
}
}

I can explicitly annotate x: [> #a | #b] in g, but it makes the switch non-exhaustive, and moreover, it is not a general solution as x can no longer be applied with other functions accepting different poly variants, e.g., [> #c].

What is causing this problem? How can I bypass this?

That what youâ€™re perceiving as a problem is exactly by design. Variants (polymorphic or not, Iâ€™m using the term loosely in this context) will always require exhaustive checks/pattern matching. Inversely, singular variants will be their own type and will not be interchangeable with each other. i.e type a = [#a] is not the same type as type b = [#b]. That is what your second example defines - the set function inside g() accepts only type [#a].

I suspect my example 1 and your first exampleâ€™s f function

let f = v => {
let x = ref(#b)
x := v
}

has something to do with block scoping and the rule â€śThe value of the last line of a scope is implicitly returned.â€ť and how the type inference deduces the types in a block scope vs the types in a module. Can someone else explain if this behavior is correct?

I donâ€™t think my first example is an error; I wrote it that way deliberately to make the typechecker infer the type of x in f to be ref<[> #b]>, and the type of x in g be [> #a]. That way, f can accept something more than #b, while inside g, the switch expression is still exhaustive. I believe this is because the type checker unifies the types with the most general types.

That is why I am puzzled with the second example, as I wrote it (almost) the same way as I have done with the first exampleâ€”so I think the second example is misleading for me. Whatâ€™s more puzzling is that the following type checks:

let f = v => {
let (_x, setX) = React.useState(() => #b)
setX(_ => v)
}
let g = () => {
let (x, _setX) = React.useState(() => #a)
f(x)
/* switch x {
| #a => f(x)
} */
}

and this too:

let f = v => {
let (_x, setX) = React.useState(() => #b)
setX(_ => v)
}
let g = () => {
let x = #a
switch x {
| #a => f(x)
}
}

I donâ€™t feel like an expert on this topic, but I think Iâ€™m able to explain the issue:
I adapted your example in the playground to not use React.useState but a custom function having the same signature:

/* previous example given by Zeta611
let f = v => {
let x = ref(#b)
x := v
}
*/
module State = {
/*
naive implementation of usestate
mind that this implementation probably does not have the desired runtime behaviour,
but has the same type signature as React.useState
*/
let use: (unit => 'a) => ('a, ('a => 'a) => unit) = init => {
let x = ref(init())
let setX = f => x := f(x.contents)
(x.contents, setX)
}
}
/* infered as [> #b] => unit */
let f2 = v => {
let (_x, setX) = State.use(() => #b)
setX(_ => v)
}
let g = () => {
let (x, _setX) = State.use(() => #a) // x is infered as: [#a]
switch x {
| #a => f2(#a)
| #a => f2((x :> [#a | #b]))
/* following fails with:
| #a => f2(x)
This has type: [#a]
But this function argument is expecting: [> #b]
The first variant type does not allow tag(s) #b
*/
}
}

The Problem

setX inside of the function f2 infers as ([> #b] => [> #b]) => unit:

init function sets x just to #b

f2â€™s argument v is inferred as any polymorphic variant constructor (due to x being a polymorphic variant)

hence the compiler will infer a lower bound for x: [> #b] - â€śat least the constructor bâ€ť

x inside of the function g infers as [#a] (without any bounds):

init function sets x just to #a

there isnâ€™t any possibility of x being something else than #a

hence the compiler will infer just #a - â€śx can only hold the constructor aâ€ť

the error message This has type: [#a] But this function argument is expecting: [> #b] The first variant type does not allow tag(s) #b:

when you call f2(x) in the function g, you are saying â€śpass xâ€ť (typed as [ #a ]) â€śto a function f2â€ť (typed as [> #b] => unit)

a value of type [ #a ] can never have the shape of #b, while [> #b ] requires the expression to be â€śat leastâ€ť possible to take on the shape of #b. which is not possible for x in g - hence the error message

Possible Solutions

I. One possible solution to this problem - although it might not be what youâ€™re looking for exactly - is to call f2 with the same constructor you already pattern matched on. This way the newly created value will be inferred as [> #a | #b ] and thatâ€™s a type you are able to pass to f2.

II. Another possible solution would be to coerce the type of x inside of g like this: f2((x :> [#a | #b])) - â€śoverrulingâ€ť the inference.

III. Alternatively you should think about your use case, and if other thechniques would be a better fit.

It could be easier to recommend a fitting solution, knowing more about your actual use case:

Who would consume the actual state you store in f?

Why do you need polymorphic variants? What are you trying to achieve?

Thank you for the thorough reply, I really appreciate it!

However I am still puzzled by:
" * there isnâ€™t any possibility of x being something else than #a"
If I slightly modify g:

let g = () => {
let (x, _setX) = State.use(() => #a) // x is infered as: [> #a]
switch x {
| _ => f2(#a)
}
}

x is now infered as [> #a]. I suspect the exhaustiveness check of the switch cases forces x to have a closed type [ #a ]; I could reproduce the same behavior with an OCaml version of this code. Iâ€™m not really sure what this â€śfeatureâ€ť is trying to prevent.

My use case isâ€¦ again with GraphQL, where my React component only uses [ #asc | #desc ], while the ppx-generated MyQuery.use({orderBy: orderBy}) expects me to pass

So probably it is best to alias the above lengthy type to something like order and use :> order.
Iâ€™m not completely satisfied with this solution, as the use of the GraphQL API in a separate function leaks the implementation detail, contaminating other functions.