United Kingdom: +44 (0)208 088 8978

F# 9: Nullable reference types

Everything you need to know about the new null-checking features of the F# 9 compiler!

We're hiring Software Developers

Click here to find out more

This post is part of our series about F# 9. Check out the full series overview for more posts about shiny new F# features!

Did you hear the good news? Nullable reference types have been improved with the release of F# 9.

What is null anyway?

The special null value is given to reference types when they have not been assigned a value.

It is purposefully hard to create in standard F#, as the concept of having data which may not exist is handled using the Option type. To demonstrate, consider the code below. The F# compiler will complain if you try to assign null to an F# type:

type Fruit =
    | Orange
    | Banana

// This won't compile: The type 'Fruit' does not have 'null' as a proper value
let someFruit: Fruit = null

Non-F# types can be assigned null from F# code…

// Neither of these cause compile errors
let someString: string = null
let someList: System.Collections.Generic.List<_> = null

Although possible, frequent use of null assignment with non-F# types is generally not recommended!

Why do we care about null if it is so rare in F#?

Null is quite common in C#. Although C# devs will minimise null propagation (hopefully! 🤞), the fact remains that it exists.

Sometimes there is no suitable F# library for something you need to do; but there might be a C# one, and all of a sudden you find yourself doing C# to F# interop! And that comes with the risk of rogue nulls infiltrating your lovely, previously null-free F# code.

How to handle null in F#

In short, check for it and recover ASAP. This means detecting it for example in an if or match statement and taking appropriate action.

In the past F# would force you to box potentially null values before they could be matched.

// This won't compile: The type 'Fruit' does not have 'null' as a proper value
let isFruitNull (f: Fruit) = f = null

// This will compile as expected
let isFruitNullBoxed (f: Fruit) = box f = null

New syntax for F# 9

The new syntax is somewhat reminicscent of TypeScript's unions, using a pipe (|) between the types. You can specify that a value could be null in the type signature, by adding | null (or null | as preferred).

// New syntax for F# 9, allowing a more streamlined experience
let isFruitNull (f: Fruit | null) = f = null

Of course you might not want to make an isNull function for every type, but with the new syntax you can also use null in pattern matching directly:

let getColourNullSafe (f: Fruit | null) =
    match f with
    | null -> "There is no fruit"
    | Orange -> "Orange"
    | Banana -> "Yellow"

let todaysLunch : Fruit | null = Orange
let tomorrowsLunch : Fruit | null = null

getColourNullSafe todaysLunch |> printfn "%s" // prints "Orange"
getColourNullSafe tomorrowsLunch |> printfn "%s" // prints "There is no fruit"

Null checking

The F# 9 compiler can check for nulls but this is turned off by default.

To switch it on, add <Nullable>enable</Nullable> in your .fsproj or --checknulls+ on the command line. If you want to enable it for F# interactive in conjunction with VS Code you can enable it by adding the following to your settings.json:

"FSharp.FSIExtraInteractiveParameters": [
    "--checknulls+"
],
"FSharp.FSIExtraSharedParameters": [
    "--checknulls+"
]

An example of the sort of error message you might see with these checks enabled:

type UserId = | UserId of int
let getUserIdValue (UserId v) = v

let userId: UserId | null = UserId 12345

// Nullness warning: The types 'UserId' and 'UserId | null' do not have equivalent nullability.
printfn $"User ID value is {getUserIdValue userId}"

The warning message doesn't appear at all unless the <Nullable>enabled</Nullable> flag is set.

Teaching an old dog new tricks

The new F# 9 compiler can provide these benefits even for projects which don't target .NET 9! If this seems confusing, then you should know that SDK version is separate to runtime version - you can use new tools (that's the SDK) and target older runtimes - especially useful if you prefer to target LTS versions, such as .NET 8. Check out this article Matt wrote for a more detailed look at SDK version vs runtime version.

Option, the alternative approach

An alternative to this kind of null checking is to use Option.ofObj to convert from nullable value to Option at the boundaries, but it is a matter of personal preference.

Here is the above example extended with another getColour variation using Option.ofObj:

let getColour (f: Fruit) =
    match f with
    | Orange -> "Orange"
    | Banana -> "Yellow"

let tryGetColour (f: Fruit) =
    f
    |> Option.ofObj // <- this converts null to None, and f to Some f
    |> Option.map getColour

tryGetColour todaysLunch |> printfn "%A" // prints Some "Orange"
tryGetColour tomorrowsLunch |> printfn "%A" // prints None

To be clear - this alternative way is not new to F# 9.

Summary

The new null features released in F# 9 should make interop between languages simpler, which is a win for everyone!