F# type inference
last modified May 17, 2025
In this article, we explore type inference in F#—a key feature that simplifies coding while ensuring type safety.
F# boasts a powerful type inference system that automatically determines the types of values, variables, and functions without requiring explicit annotations. This makes F# code more concise and readable while maintaining strong type safety. The compiler examines the code and infers the most general types that accommodate all possible usages, allowing developers to focus on logic rather than manual type definitions.
By leveraging type inference, F# reduces boilerplate code and enhances maintainability, making it particularly well-suited for functional programming. The system ensures type correctness at compile-time, minimizing runtime errors and improving overall code reliability. While explicit type annotations are optional, they can still be used when needed for clarity or performance optimizations.
Basic type inference
F# can infer types from literals and simple expressions. The compiler starts with known types (like literals) and propagates that information through the expression.
// Integer inference let x = 42 printfn "Type of x: %s" (x.GetType().Name) // Float inference let y = 3.14 printfn "Type of y: %s" (y.GetType().Name) // String inference let name = "Alice" printfn "Type of name: %s" (name.GetType().Name) // Boolean inference let flag = true printfn "Type of flag: %s" (flag.GetType().Name)
This example shows how F# infers types from literals and simple expressions.
λ dotnet fsi basic_inference.fsx Type of x: Int32 Type of y: Double Type of name: String Type of flag: Boolean
Function type inference
F# excels at inferring function types based on how parameters are used. The compiler analyzes the function body to determine parameter and return types.
// Simple function inference let square x = x * x printfn "square 5 = %d" (square 5) // Multi-parameter function let joinStrings a b = a + " " + b printfn "%s" (joinStrings "Hello" "World") // Higher-order function let applyTwice f x = f (f x) let increment x = x + 1 printfn "applyTwice increment 5 = %d" (applyTwice increment 5) // Generic function inference let firstElement list = List.head list printfn "First: %d" (firstElement [1; 2; 3]) printfn "First: %s" (firstElement ["a"; "b"; "c"]) // Type annotations can help inference let mixedAdd (x:float) y = x + float y printfn "mixedAdd result: %f" (mixedAdd 3.14 2)
This code demonstrates how F# infers function types. The compiler determines
that square
works with integers, joinStrings
with
strings, and applyTwice
is a higher-order function.
firstElement
is inferred as generic.
λ dotnet fsi function_inference.fsx square 5 = 25 Hello World applyTwice increment 5 = 7 First: 1 First: a mixedAdd result: 5.140000
Type inference in collections
F# can infer collection types based on their contents and usage. The compiler analyzes element types and operations to determine the most specific collection type.
// List inference let numbers = [1; 2; 3; 4] printfn "Numbers type: %s" (numbers.GetType().Name) // Heterogeneous lists (not allowed) // let mixed = [1; "two"; 3.0] // This would cause an error // Array inference let squares = [| for i in 1..5 -> i * i |] printfn "Squares type: %s" (squares.GetType().Name) // Sequence inference let fibSeq = seq { let rec fib a b = seq { yield a yield! fib b (a + b) } yield! fib 0 1 } printfn "First 5 fib: %A" (fibSeq |> Seq.take 5 |> Seq.toList) // Generic collection functions let filterEvens list = list |> List.filter (fun x -> x % 2 = 0) printfn "Evens: %A" (filterEvens [1..10])
This example shows type inference with collections. F# determines that
numbers
is an int list, squares
is an int array, and
fibSeq
is a sequence. The compiler prevents mixing types in lists.
λ dotnet fsi collection_inference.fsx Numbers type: FSharpList`1 Squares type: Int32[] First 5 fib: [0; 1; 1; 2; 3] Evens: [2; 4; 6; 8; 10]
Record and DU type inference
F# can infer types for records and discriminated unions based on their usage. The compiler tracks field names and cases to ensure type safety.
// Record inference type Person = { Name: string; Age: int } let alice = { Name = "Alice"; Age = 30 } printfn "%s is %d years old" alice.Name alice.Age // Record field type inference let getName person = person.Name printfn "Name: %s" (getName alice) // Discriminated union inference type Shape = | Circle of radius: float | Rectangle of width: float * height: float let circle = Circle 5.0 let rect = Rectangle (4.0, 6.0) let area shape = match shape with | Circle r -> System.Math.PI * r * r | Rectangle (w, h) -> w * h printfn "Circle area: %f" (area circle) printfn "Rectangle area: %f" (area rect)
This code demonstrates type inference with records and discriminated unions. The
compiler knows alice
is a Person, getName
takes a
Person, and area
works with any Shape.
λ dotnet fsi record_du_inference.fsx Alice is 30 years old Name: Alice Circle area: 78.539816 Rectangle area: 24.000000
Best practices
While type inference reduces verbosity, strategic use of type annotations can improve code clarity and help catch errors earlier.
// Recommended annotations for public APIs module MathOperations = let add (x:int) (y:int) : int = x + y let multiply (x:float) (y:float) : float = x * y // Helpful for complex return types type Customer = { Id: int; Name: string } let getCustomers () : Customer list = [{Id = 1; Name = "Alice"}; {Id = 2; Name = "Bob"}] // Useful for interface implementations type ILogger = abstract Log : string -> unit let createLogger () : ILogger = { new ILogger with member _.Log message = printfn "LOG: %s" message } // Improves error messages for generic code let findFirst<'T> (predicate:'T -> bool) (items:'T list) : 'T option = List.tryFind predicate items // Makes test expectations clearer let shouldEqual (expected:'T) (actual:'T) = if expected = actual then printfn "OK" else printfn "FAIL: Expected %A, got %A" expected actual shouldEqual 42 (MathOperations.add 40 2)
This code demonstrates good practices for type annotations. Public APIs, complex return types, interface implementations, and generic functions often benefit from explicit types.
λ dotnet fsi best_practices.fsx OK
F#'s type inference system provides an excellent balance between conciseness and type safety. By automatically deducing types from code context, it reduces boilerplate while catching errors at compile time. Understanding how type inference works helps write more maintainable F# code and know when to add explicit type annotations for clarity.