Hi all!
So when you make type annotations on a function with optional named args, you are required to be explicit with the option in the argument type:
This is correct. It is because inline type annotations for arguments are what the inside of the function “sees.” When you type an entire function at once, then you’re typing what the outside of the function sees.
Outside of that function, you have to pass a non-option value to onChange. As soon as your value goes inside the function, it’s automatically wrapped into an option.
It’s unfortunate that this can be confusing, although I concur that it’s best to only annotate the interface anyway in general.
I have heard this response a number of times now in rescript and I cant caution enough against it.
If this language does not comfortably and reliably support the types that are its first feature, it will fail.
still seems like the inner signature could have the same form as the outer signature with the option wrapping being implied?
[maybe parenthetically] I hear lots of the grey beards here talk about interface files and I can guarantee that no new conscripts to the language ever touch them. Noone in my team has the slightest idea about them after a year and a half. It is very common though for people to give a binding an explicit type to narrow the location of a type error…and I hope that would be natural for any type without excuse
Another thought on inside/outside is that if thats the approach, then the =? element should not be allowed inside as thats not a concern inside the function?
That’s why I am suggesting using the language in the way that it comfortably and reliably supports. Types in interface files and annotation-free implementation code. It’s the best of both worlds.
Also, there are small particularities in the type system that can confuse newcomers, as you found in this case. Taking my suggestion and not annotating your implementation would have avoided hitting this corner case altogether. The type in the interface would look exactly as you expected it to. It’s because you went slightly against the grain of the language and tried to annotate the implementation, that you ran into this slight confusion.
Interface files have myriad other benefits: faster to read because you don’t have to get sidetracked by implementation details; allow controlling exported items (how are you controlling encapsulation and privacy now?), obvious place to put doc comments without littering the implementation code.
Let’s look at an analogy. If someone were to come to me with this code:
type person = {firstName: string, secondName: option<string>}
let fullName = person => {
let fn = person.firstName
if Belt.Option.isSome(person.secondName) {
let sn = Belt.Option.getExn(person.secondName)
`${fn} ${sn}`
} else {
`${fn}`
}
}
I would tell them to rewrite it in a more idiomatic way:
It’s because the language conflates optional arguments with optional types, and the implementation used by the language essentially leaks in the difference between .res and .resi. There’s indeed a little magic used by the compiler to handle external calls which shows through here.
Probably a good idea to make a note of this for uncurried calls (CC @Hongbo here) in case this confusion will be eliminated when (if) transitioning to uncurried by default.
Could it be the other way round, i.e., in the implementation, ~onChange: F.t => unit=? would be enough to convey that onChange is actually option<F.t => unit>?