100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Generic Types in F#

Explore how F# generic types and automatic type inference let you write reusable code that works across many types safely.

Type SystemIntermediate9 min readJul 10, 2026
Analogies

What Are Generic Types?

A generic type or function in F# is parameterized over one or more type placeholders, written with a leading apostrophe like 'a, 'b, so the same definition works across many concrete types without duplication. A function like let identity x = x is automatically inferred to have type 'a -> 'a, meaning it accepts a value of any type and returns a value of that same type, and a type like type Box<'a> = { Contents: 'a } can hold an int, a string, or any other type inside Contents while the compiler still tracks exactly which one at each use site.

🏏

Cricket analogy: A generic type is like a scoring template that works identically whether it's tracking Test, ODI, or T20 formats -- the structure (overs, runs, wickets) is the same, only the specific numbers change -- the way Box<'a> holds the same shape of container regardless of whether 'a is filled in as int or string.

Defining Generic Functions and Types

You rarely need to write type parameters explicitly -- F#'s type inference generalizes a function automatically based on how it's used, so let first (a, b) = a is inferred as 'a * 'b -> 'a without annotation. For types, you declare parameters after the name, as in type Pair<'a, 'b> = { First: 'a; Second: 'b }, and construct them like any other record: { First = 1; Second = "one" } gives a Pair<int, string>. Generic functions over collections are everywhere in the standard library -- List.map : ('a -> 'b) -> 'a list -> 'b list works on a list of any element type because it never inspects the elements itself, only threads them through the caller-supplied function.

🏏

Cricket analogy: List.map transforming a list of scores without inspecting them individually is like a scoreboard operator who mechanically applies a run-rate formula to every entry in the innings log regardless of which batter it belongs to, never needing to know who scored what, just the transformation rule itself.

fsharp
type Pair<'a, 'b> = { First: 'a; Second: 'b }

let swap (pair: Pair<'a, 'b>) : Pair<'b, 'a> =
    { First = pair.Second; Second = pair.First }

let p1 = { First = 1; Second = "one" }
let p2 = swap p1  // Pair<string, int>

Automatic Generalization and Type Inference

F#'s type inference doesn't just infer concrete types -- it infers the most general type possible for a let-bound function, a process called automatic generalization. Writing let listOfOne x = [x] gives the inferred signature 'a -> 'a list without any annotation, and that same function works unmodified whether you call listOfOne 5 or listOfOne "hi" in different parts of the same file. This generalization happens at the point of definition for functions, but for simple (non-function) values bound with let, F#'s value restriction can prevent generalization in certain cases, requiring either an explicit type annotation or eta-expansion into a function to resolve.

🏏

Cricket analogy: Automatic generalization inferring 'a -> 'a list for listOfOne without annotation is like a cricket board writing tournament rules generically enough at the moment of drafting that they apply unmodified to men's, women's, and under-19 competitions without needing separate rulebooks for each.

Constraints on Generic Type Parameters

Sometimes a generic function needs to do more than just move values around -- comparing them, for instance -- which requires a constraint. F# adds these automatically in many cases: let maxOf a b = if a > b then a else b infers as 'a -> 'a -> 'a when 'a : comparison, restricting 'a to types that support the > operator, so calling maxOf 3 5 works but maxOf (fun x -> x) (fun y -> y) fails to compile because function types don't support comparison. Explicit constraint syntax like let inline addOne (x: ^a) : ^a = x + LanguagePrimitives.GenericOne uses statically resolved type parameters (denoted with ^a instead of 'a) to require that 'a supports the + operator, resolved separately for each concrete type at each call site during compilation.

🏏

Cricket analogy: A comparison constraint on maxOf is like an ICC ranking system that can only rank formats where a numeric rating exists -- Test, ODI, T20 -- but can't 'rank' something like ground maintenance quality that has no defined ordering, just as maxOf rejects types without a > operator.

fsharp
let maxOf a b = if a > b then a else b
// inferred: 'a -> 'a -> 'a when 'a : comparison

let inline addOne (x: ^a) : ^a =
    x + LanguagePrimitives.GenericOne

let a = addOne 5        // int
let b = addOne 5.0      // float

Statically resolved type parameters, written ^a instead of 'a, let inline functions require an operator or member (like +) without needing an interface constraint -- the compiler resolves a separate concrete implementation for each type used at each call site during compilation, a technique sometimes called 'compile-time duck typing.'

The value restriction can prevent a non-function let binding like let empty = [] from generalizing automatically; F# instead infers a single, as-yet-undetermined type for 'a that gets fixed by the first use. If you see an unexpected 'FS0030 value restriction' error, either add an explicit type annotation or rewrite the binding as a function, e.g. let empty () = [].

  • Generic types and functions are parameterized with 'a, 'b, etc., and work across many concrete types without duplication.
  • F# infers the most general type automatically -- you rarely write type parameters explicitly.
  • type Pair<'a, 'b> = { First: 'a; Second: 'b } can hold any two types at once.
  • Some generic functions carry implicit constraints, like when 'a : comparison, derived from how the type is used.
  • Statically resolved type parameters (^a) support inline functions that require an operator like +.
  • The value restriction can block generalization for non-function let bindings.
  • Standard library functions like List.map are generic because they never inspect element values directly.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#GenericTypesInF#Generic#Types#Defining#Functions#StudyNotes#SkillVeris#ExamPrep