I have the following snippet with full type annotation.
type context = string
type result<'a> =
| Ok({parsed: 'a, newContext: context})
| Err(string)
let map = (r: result<'a>, fn: 'a => 'b): result<'b> =>
switch r {
| Ok({parsed, newContext}) => Ok({parsed: fn(parsed), newContext: newContext})
| e => e
}
let flatMap = (r: result<'a>, fn: ('a, context) => result<'b>): result<'b> =>
switch r {
| Ok({parsed, newContext}) => fn(parsed, newContext)
| e => e
}
I expect map to have type (result<'a>, 'a => 'b) => result<'b> and flatMap to have type (result<'a>, ('a, context) => result<'b>) => result<'b>. However, the compiler insists that map has type (result<'b>, 'b => 'b) => result<'b> and flatMap has type (result<'b>, ('b, context) => result<'b>) => result<'b>.
Could anyone please show me what I’m doing wrong here?
The error cases in both switch statements are wrong. They return the original value - result<‘a> - not the type you’re expecting. You need to re-wrap the string in a new Error constructor to create a new value of a different type.
Polymorphic type annotations in the implementations actually are not universally quantified. This means that, even though you said that these functions have two type parameters 'a and 'b, if you get the implementation wrong the compiler will happily unify the type parameters to 'b like you saw in your case.
The best way to avoid this error is to actually not annotate the implementation, and put the type annotations in an interface file, where type parameters will be universally quantified.
By the way, you don’t really need this custom result type, you can alias the existing one, e.g.
type ok<'a> = {parsed: 'a, newContext: context}
type myResult<'a> = result<ok<'a>, string>
I see inline type annotations a lot in people coming from TypeScript, in ReScript they’re almost always unnecessary on function arguments and they’re both unnecessary and misleading on return types. This is why I recommend avoiding them.