Not having to annotate types for every argument and return value in F# is a big relief. In this blog post, I share some tips and tricks to reduce the need for type annotations.
This blog post is part of the F# advent calendar 2025. Thanks, Sergey, for the organisation!
In F#, we don’t need to annotate every type for function arguments or function return types as in most other typed languages. Type inference can automatically determine the correct type.

In the above example, f must be a function that takes a decimal because it is used with a decimal in y. And y return obviously a decimal as well.
However, type inference isn’t magic and has its limits. With some tricks, we can help the compiler.
Why should I even care?
If you are used to annotating types everywhere, you might ask why you should care to reduce the number of type annotations at all.
Refactoring
Using type inference has advantages when refactoring. When I change the returned type in a chain of function calls, the changed type “bubbles” up the chain automatically.

If I change the code inside the refactorMe function to return x, all calling functions get a new return type of a' without needing to change them. In a programming language that requires type annotations, I would have to change all function return types (hopefully the IDE helps with this).
Readability
Leaving out type annotations makes the code more readable because the code’s intent comes into focus. If I need to know a type, the IDE helps me out with its type hints.
Order matters
In the following code, a has type AnotherType because it is defined closer to the definition of a.

If I want to have a to have type AType, I need to add a type annotation – or do I?
Of course, in this small example, I could rearrange the type definitions. But that would break code that needs a value of type AnotherType.
A better approach is to put both the type definitions in different namespaces or modules. Then, I can decide locally which namespace or module to open, and, if both are needed, in which order:

Because I open module A last, the value a gets the type AType despite AnotherType being defined later.
Using (module) functions to specify the type
Another way to specify the type of an input argument is by passing it to a function that already defines the type.
In the following code example, createAListOfNames has an input argument employees. I don’t have to annotate the type because the function Employee.getName already defines it.

To keep the function(s) in the Employee module free of type annotations, the module should be defined as close as possible after the type(s) it uses.
This works, however, only when the first function that the value is passed to defines the type.
Side note: I use FSharp.UMX to give the properties of the Employee record more meaning (string<Name>, string<Email>).
Summary
I showed you two small tips on how to make your code a bit easier on the eyes and more straightforward to refactor by reducing the number of type annotations.
That’s it for today.
If you think that was too short an article, you can always read my last year’s post, which is very long: Your First day on the team = releasing Your first feature – planetgeek.ch
Merry Christmas and a happy New Year!