In ML-lineage languages (including Haskell) you almost never need any type annotations whatsoever, at least not unless you’re poking around at the fringes of those languages (GADTs, various GHC extensions).
Type annotations for top-level definitions are often encouraged for readability and better error messages, but the compiler can almost always figure everything out itself.
From my experience, such type inference systems are awful in practice. Rust designers tried to do something like that initially but quickly realized understandability suffered greatly. You really do want to specify types manually, at least at boundaries, e.g. in function definitions.
> Type annotations for top-level definitions are often encouraged for readability and better error messages, but the compiler can almost always figure everything out itself.
See? That's not a good thing at all. If the compiler's capability makes the code less understandable, then it's undesirable. Doesn't matter how fancy and cool, or state-of-the-art it may be.
You probably don't want to strap a jet engine to a car, no matter how cool you may think it would be.
Right, global inference turns out to be too much inference. Function boundaries are a convenient place to draw a line.
As usual C++ choose to something much weirder and more dangerous. Instead of inference C++ can deduce types, in some cases there's no way to write a type's name so you have to deduce types, and they can be deduced at the edges of functions however unlike inference it's not an error to have ambiguity, in some cases deduction may choose one of the possibilities that it liked better even if that's astonishing for you.
Because Rust's functions must tell you their types explicitly, and because some types can't be named specifically, the result is that in Rust you have to write these functions polymorphically, even if in practice there's only one possibility. In C++ you can write the non-polymorphic function, despite not being able to say the name of the type. How do you document that? It's OK, C++ doesn't require you to provide even halfway usable documentation.
> You really do want to specify types manually, at least at boundaries, e.g. in function definitions
I think PureScript has the best compromise here, top-level type signatures are not enforced but if you don't include one you'll get a warning with the inferred signature. On the one hand, this is very helpful because sometimes I have a grasp for what expression I want to use, but am not sure about its type, so I simply comment out the desired signature and let the compiler tell me which direction I'm moving in, i.e. what's the type of what I just wrote. On the other hand, it being a warning also basically ensures nobody writes their top-level functions without the type signatures. Really the best of the two worlds. I think simply disallowing top-level functions without type signatures would hurt my workflow a lot.
That’s just artificial limits. You yourself mentioned Rust that does the exact same type inference with the limit for usability of having signatures typed. Here is your example of a language with eons better type system/type inference, but go’s is really not a high mark.
Type annotations for top-level definitions are often encouraged for readability and better error messages, but the compiler can almost always figure everything out itself.