ZetCode

F# obj type

last modified May 17, 2025

F# is a functional-first language with strong type safety, yet sometimes you need to work with generic object types (obj), which act as flexible containers for handling various types dynamically. This tutorial explores how and when to use obj in F#, including practical examples and thread safety considerations.

Understanding obj in F#

In F#, obj is an alias for System.Object, the base type of all .NET objects. This means that every value type (int, float, bool) and reference type (string, list, array) inherits from obj.

Key Uses of obj in F#

  1. Universal Base Type - Since all .NET types inherit from System.Object, obj can store values of any type.
  2. Boxing & Unboxing - Value types (like int, float, bool) are boxed into obj, meaning they are stored as objects. Unboxing restores their original type.
  3. Working with Reflection - Since obj is the base type, it is often used in reflection when handling unknown types at runtime.
  4. Polymorphism in .NET Interoperability - Some .NET APIs expect obj parameters for flexibility in handling multiple types.
basic_usage.fsx
let x: obj = "Hello, F#"  // Assigning a string to obj
let y: obj = 42           // Assigning an integer to obj

printfn "%s" (x :?> string)  // Unboxing the string
printfn "%d" (y :?> int)      // Unboxing the integer

let number: obj = 10  // Boxing integer
let unboxedNumber = number :?> int  // Unboxing
printfn "%d" unboxedNumber

This shows basic boxing and unboxing operations. The :?> operator performs a downcast (unboxing) that may throw an exception if types don't match.

λ dotnet fsi basic_usage.fsx
Hello, F#
42
10

Interacting with .NET APIs

Many .NET APIs expect parameters of type obj. This example retrieves runtime type information.

type_info.fsx
open System

let printTypeInfo (value: obj) =
    let valueType = value.GetType()
    printfn "Value: %A, Type: %s" value valueType.FullName

printTypeInfo 123            // int
printTypeInfo 3.14           // float
printTypeInfo "F# is great!" // string
printTypeInfo (DateTime.Now) // System.DateTime

This function prints the value and its type using GetType. The FullName property provides the full name of the type.

λ dotnet fsi type_info.fsx
Value: 123, Type: System.Int32
Value: 3.14, Type: System.Double
Value: "F# is great!", Type: System.String
Value: 5/17/2025 2:30:45 PM, Type: System.DateTime

Storing Heterogeneous Data

Using obj allows storing different types in collections.

generic_dictionary.fsx
open System.Collections.Generic

let dataStore = Dictionary<string, obj>()

dataStore["name"] <- "Alice"
dataStore["age"] <- 30
dataStore["isMember"] <- true

match dataStore["name"] with
| :? string as name -> printfn "User Name: %s" name
| _ -> printfn "Invalid type"

match dataStore["age"] with
| :? int as age -> printfn "User Age: %d" age
| _ -> printfn "Invalid type"

This example demonstrates how to store heterogeneous data in a dictionary. The Dictionary<string, obj> allows storing different types of values, and pattern matching is used to retrieve them safely.

λ dotnet fsi generic_dictionary.fsx
User Name: Alice
User Age: 30

Handling Dynamic Objects

When working with dynamic data sources like JSON, obj can be useful before proper deserialization.

dynamic_handling.fsx
let processDynamicData (data: obj) =
    match data with
    | :? string as s -> printfn "String length: %d" s.Length
    | :? int as i -> printfn "Number squared: %d" (i * i)
    | :? (int list) as lst -> printfn "List sum: %d" (List.sum lst)
    | _ -> printfn "Unknown type"

processDynamicData "hello"
processDynamicData 5
processDynamicData [1..5]

This function processes dynamic data and performs different actions based on the type. The List.sum function computes the sum of a list of integers.

λ dotnet fsi dynamic_handling.fsx
String length: 5
Number squared: 25
List sum: 15

Type Testing and Casting

F# provides several ways to work with obj types safely.

type_casting.fsx
open System
let safeUnbox (example: obj) =
    match example with
    | :? string as s -> printfn "It's a string: %s" s
    | :? int as i -> printfn "It's an int: %d" i
    | :? float as f -> printfn "It's a float: %f" f
    | _ -> printfn "Unknown type"

safeUnbox (box "Hello")
safeUnbox (box 42)
safeUnbox (box 3.14)
safeUnbox (box DateTime.Now)

// Using tryUnbox pattern
let tryUnbox<'T> (o: obj) =
    match o with
    | :? 'T as t -> Some t
    | _ -> None

let result = tryUnbox<int> (box "hello")
printfn "Unbox result: %A" result

The box function explicitly boxes a value, while pattern matching with :? tests types safely.

λ dotnet fsi type_casting.fsx
It's a string: Hello
It's an int: 42
It's a float: 3.140000
Unknown type
Unbox result: None

Thread Safety with obj

When working with mutable state and obj in concurrent scenarios, ensuring proper synchronization is essential to prevent race conditions. When multiple threads modify shared mutable objects simultaneously without locking mechanisms, unexpected behavior can occur, such as inconsistent values or data corruption. F# provides lock-based synchronization techniques that ensure safe modifications to shared state, maintaining data integrity.

The example below illustrates the difference between unsafe mutable access, where multiple threads modify a shared variable without synchronization, and thread-safe updates, where a locking mechanism ensures proper updates. By using lock, we enforce atomic operations, ensuring that multiple threads update shared state in a controlled manner.

thread_safety.fsx
open System.Threading

// Unsafe mutable access
let mutable unsafeCounter = 0

let incrementUnsafe() =
    for _ in 1..100000 do
        unsafeCounter <- unsafeCounter + 1

// Thread-safe counter
let safeCounter = ref 0
let lockObj = obj()

let incrementSafe() =
    for _ in 1..100000 do
        lock lockObj (fun () -> 
            safeCounter.Value <- safeCounter.Value + 1)

let t1 = Thread(incrementUnsafe)
let t2 = Thread(incrementUnsafe)

t1.Start()
t2.Start()
t1.Join()
t2.Join()

printfn "Unsafe counter: %d" unsafeCounter

let t3 = Thread(incrementSafe)
let t4 = Thread(incrementSafe)

safeCounter.Value = 0
t3.Start()
t4.Start()
t3.Join()
t4.Join()

printfn "Safe counter: %d" safeCounter.Value

Using locks ensures that shared mutable state is updated correctly across multiple threads. The unsafe approach may lead to inconsistent values due to race conditions, whereas the safe approach guarantees atomic updates to prevent conflicts. Employing synchronization mechanisms like locks, immutability, or thread-safe collections is crucial when working with concurrent mutable state.

λ dotnet fsi thread_safety.fsx
Unsafe counter: 117532
Safe counter: 200000

The obj type in F# provides necessary flexibility when working with dynamic data, .NET interop, and reflection scenarios. While F# encourages strong typing, understanding how to properly use obj, boxing/unboxing, and type casting is essential for certain programming tasks.

Prefer type-safe alternatives like discriminated unions or records when possible. Use obj only when necessary, and document its usage clearly. This helps maintain code clarity and safety. Avoid using obj for performance-critical code, as boxing and unboxing can introduce overhead.

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.