ZetCode

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.

basic_inference.fsx
// 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.

function_inference.fsx
// 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.

collection_inference.fsx
// 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_du_inference.fsx
// 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.

best_practices.fsx
// 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.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.