Skip to content

F# Guide — Records: Structured Data with Named Fields

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

F# records are user-defined data types that aggregate values with named fields, providing immutability by default, structural equality, copy-and-update syntax, and seamless integration with pattern matching.

What You'll Learn

  • Defining and using record types
  • Structural equality and comparison
  • Copy-and-update expressions
  • Records in pattern matching
  • Record vs class differences

Why It Matters

Records are the primary way to model structured data in F#. They combine the simplicity of data containers with functional features like immutability and pattern matching. Durga Antivirus Pro uses records for configuration and rule models.

Real-World Use

Records model domain entities like users, orders, products, and configuration. They are preferred over classes for data-centric types.

flowchart LR
    A["Records"] --> B["Definition"]
    B --> C["Construction"]
    C --> D["Copy & Update"]
    D --> E["Pattern Matching"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Defining Records

// Simple record
type Person = {
    Name: string
    Age: int
    Email: string
}

// Record with type parameters
type Result<'T> = {
    Value: 'T
    Timestamp: System.DateTime
}

// Record with member
type Point = {
    X: float
    Y: float
} with
    member this.DistanceFromOrigin =
        sqrt (this.X ** 2 + this.Y ** 2)

Creating Records

// Field order
let alice = { Name = "Alice"; Age = 30; Email = "alice@example.com" }

// Type annotation if ambiguous
let bob: Person = { Name = "Bob"; Age = 25; Email = "bob@example.com" }

// Clone with defaults
type Config = {
    Host: string
    Port: int
    Debug: bool
}
let defaultConfig = { Host = "localhost"; Port = 8080; Debug = false }

Accessing Fields

let person = { Name = "Alice"; Age = 30; Email = "alice@example.com" }

// Dot notation
person.Name    // "Alice"
person.Age     // 30

// Destructuring in let
let { Name = name; Age = age } = person
// name = "Alice", age = 30

// Destructuring in function parameters
let greet { Name = name } = sprintf "Hello, %s!" name

Structural Equality

// Records have structural equality by default
let a = { Name = "Alice"; Age = 30; Email = "a@example.com" }
let b = { Name = "Alice"; Age = 30; Email = "a@example.com" }

a = b  // true (structural equality)
a <> b  // false

// This is different from classes (reference equality)
// Useful for comparison and dictionary keys

Copy-and-Update

let alice = { Name = "Alice"; Age = 30; Email = "alice@example.com" }

// Create copy with changed fields
let olderAlice = { alice with Age = 31 }

let nameChange = { alice with Name = "Alice Smith" }

// Multiple changes
let moved = { alice with Email = "alice@newdomain.com"; Age = 31 }

// Original is unchanged
// alice.Age = 30 still

Records in Collections

type Product = { Id: int; Name: string; Price: decimal }

let products = [
    { Id = 1; Name = "Laptop"; Price = 999.99m }
    { Id = 2; Name = "Mouse"; Price = 29.99m }
    { Id = 3; Name = "Keyboard"; Price = 79.99m }
]

let expensiveProducts =
    products |> List.filter (fun p -> p.Price > 100m)

let totalValue =
    products |> List.sumBy (fun p -> p.Price)

Nested Records

type Address = {
    Street: string
    City: string
    ZipCode: string
}

type Employee = {
    Name: string
    Address: Address
    Department: string
}

let emp = {
    Name = "Alice"
    Address = { Street = "123 Main"; City = "Portland"; ZipCode = "97201" }
    Department = "Engineering"
}

// Nested update
let relocated = { emp with Address = { emp.Address with City = "Seattle" } }

Common Mistakes

1. Forgetting all field values

Records require all fields during construction. No optional fields exist (use option types).

2. Ambiguous record types

Two record types with same field names cause ambiguity. Use type annotations or different field names.

3. Expecting mutation

Records are immutable. Use copy-and-update for modifications.

4. Overusing record members

Keep records as simple data containers. Add methods sparingly.

5. Confusing records with anonymous records

F# also has anonymous records {| Name: string |} with different semantics (no type definition needed).

Practice Questions

1. What is structural equality? Two records with the same field values are equal. Unlike classes, which use reference equality by default.

2. How do you create a copy with modified fields? Use copy-and-update: { originalRecord with Field = newValue }.

3. Can records have methods? Yes, use with member this.MethodName = ... in the type definition.

Challenge: Design a record hierarchy for an e-commerce system with Customer, Order, and Product records.

FAQ

{{< faq question="Are records reference types?" >}} Yes, F# records are reference types stored on the heap. For value-type records, add [<Struct>] attribute. {{< /faq >}}

{{< faq question="Can records implement interfaces?" >}} Yes, records can implement interfaces using the interface ... with syntax. {{< /faq >}}

{{< faq question="Do records support inheritance?" >}} No. Records cannot inherit from other records. Use discriminated unions or classes for inheritance hierarchies. {{< /faq >}}

{{< faq question="How are records different from tuples?" >}} Records have named fields (self-documenting) and can have many fields. Tuples are positional and best for 2-3 values. {{< /faq >}}

{{< faq question="Can records be mutable?" >}} Yes, add mutable keyword to fields: { mutable Name: string }. But this goes against F# idioms. {{< /faq >}}

Mini Project

Build a contact management system with records:

type Phone = { Type: string; Number: string }
type Contact = {
    Name: string
    Email: string option
    Phones: Phone list
}

let contacts = [
    { Name = "Alice"; Email = Some "alice@example.com"; Phones = [{ Type = "Mobile"; Number = "555-0100" }] }
    { Name = "Bob"; Email = None; Phones = [{ Type = "Home"; Number = "555-0200" }; { Type = "Work"; Number = "555-0201" }] }
]

let getEmailOrPhone contact =
    match contact.Email with
    | Some email -> email
    | None -> (List.head contact.Phones).Number

What's Next

Now that you understand records, explore discriminated unions for modeling choices and variants.

Topic Description Link
F# Discriminated Unions Union types {{< ref "11-discriminated-unions" >}}
F# Option Types Option type deep dive {{< ref "12-option-types" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro