fsharp-nullness-interop
Skill23 starsChanged 32 days agoMITView on GitHub
Handle F# nullness checking (Nullable enable, FS3261, FS3262 in F# 9 and later) at .NET interop boundaries, and fix FS0041 "A unique overload could not be determined" from new BCL overloads such as TimeSpan.FromSeconds(int64). Covers BCL members that return `| null`, correct null handling with match and Option.ofObj, null leaks that the compiler does not report, and when suppression is acceptable. Use when an F# build reports FS3261, FS3262, or FS0041, when the user turns on nullness checks or TreatWarningsAsErrors, or when F# code calls .NET APIs that can return null.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- fsharp-async-compositionSkill
- fsharp-binary-compatibilitySkill
- fsharp-env-capabilitiesSkill
- fsharp-failure-pathsSkill
- fsharp-idiom-reviewSkill
- fsharp-iwsam-errorsSkill
- fsharp-library-api-designSkill
- fsharp-native-aotSkill
- fsharp-performance-typesSkill
- fsharp-serializationSkill
- fsharp-signature-filesSkill
- fsharp-verify-apiSkill
- optimize-fsharp-typecheck-graphSkill
- parse-dont-validateSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 0 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 71
Do not mix | null -> with | NonNull v ->.
- Very specific: line 95
Use Option.ofObj instead of Some on a .NET result.
- Very specific: line 100
Use nonNull or Unchecked.nonNull only when the API cannot return null in this call, and the type does not say so.
- Very specific: line 132
Search the changed code for nonNull, #nowarn, Some, and string used as a function (string x, x |> string).
- Very specific: line 134
Run the code with null inputs at each boundary: a JSON null, an unset environment variable, an exception with no inner exception, and an …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
F# nullness at interop boundaries
F# nullness checking makes .NET reference types that can be null visible as T | null.
The compiler reports most problems. The risk is the wrong fix: a fix that compiles and
hides a real null. Convert each T | null to an F# type once, at the boundary. Then the
rest of the code uses T or T option.
scripts/run.ps1 verifies the null results and diagnostics below. Verify lists the rules that it does not verify.
Turn on the checks
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup><Nullable>enable</Nullable> passes --checknulls+. Nullness diagnostics are warnings
(FS3261, FS3262). TreatWarningsAsErrors makes them fail the build.
Members that return | null
These members return T | null. Using the result as T gives FS3261.
| Member | Null when |
JsonElement.GetString() | the JSON value is null |
JsonSerializer.Deserialize<T> | the document is null, also for F# records |
Uri.TryCreate(s, kind) out value | the parse fails, so it is typed Uri | null even on true |
Environment.GetEnvironmentVariable | the variable is not set |
exn.InnerException | there is no inner exception |
Path.GetDirectoryName | the path is a root, such as C:\ or /. A bare file name gives "", not null. |
TextReader.ReadLine(), Console.ReadLine() | end of input |
obj.ToString(), Type.FullName | the API allows null |
Handle null at the boundary
| Need | Use |
| Value or fallback | match x with | null -> fallback | v -> v |
| Null and empty both mean "none" | match x with | null | "" -> None | v -> Some v |
option | x |> Option.ofObj |
Result | match x with | null -> Error "..." | v -> Ok v |
Out value of a Try method | | true, NonNull u -> ... |
| Two named cases | match x with | Null -> ... | NonNull v -> ... |
let tryHost (s: string) : string option =
match Uri.TryCreate(s, UriKind.Absolute) with
| true, NonNull u -> Some u.Host
| _ -> None
let rec rootMessage (ex: exn) : string =
match ex.InnerException with
| null -> ex.Message
| inner -> rootMessage innerRules that the build does not tell you in advance:
isNull xandString.IsNullOrEmpty xdo not narrowstring | nulltostring. Theelsebranch still gives FS3261. Usematch.- Do not mix
| null ->with| NonNull v ->. After| null, the value is already non-null.NonNullthen gives FS3262 and the match gives FS0025. Use| v ->, or useNullandNonNulltogether. - Annotate public return types. Without an annotation, the compiler infers
T | nulland moves the null to the caller. - A
stringbinding cannot holdnull.let mutable x: string = nullgives FS3261. Declarestring | null, or better, usestring option. - Passing
ex.InnerExceptionto a parameter of typeexngives FS3261 with the textThe types 'exn' and 'exn | null' do not have compatible nullability. Match on the value before the call, or declare the parameter asexn | nulland match inside.
Null leaks that compile clean
The compiler does not report these. Each one compiles with nullness and
TreatWarningsAsErrors on.
| Code | Result for null |
Some(reader.ReadLine()) with no type annotation | Some null, not None. The type becomes (string | null) option. |
string x on string | null | "". A missing value becomes an empty string. |
nonNull x | throws NullReferenceException |
Unchecked.nonNull x | passes the null on. It fails later, far from the cause. |
#nowarn "3261", <NoWarn>FS3261</NoWarn> | hides every later null in the file or project |
Use Option.ofObj instead of Some on a .NET result. Use string x only when an empty
string is the correct value for null.
When suppression is acceptable
Use nonNull or Unchecked.nonNull only when the API cannot return null in this call,
and the type does not say so. Write the reason in a comment on the same line or the line
above. Example: a value that a Try method returned with true, when you cannot use
NonNull in the pattern. Never suppress FS3261 for a file or a project to get a build
to pass.
NonNullQuick and nullArgCheck throw on null like nonNull. They need the same written reason.
// GetEntryAssembly is null only in unmanaged hosts. This exe always has one.
let entry = Unchecked.nonNull (Assembly.GetEntryAssembly())New BCL overloads (FS0041)
.NET 9 added int64 overloads to TimeSpan.FromSeconds, FromMinutes,
FromMilliseconds, and FromMicroseconds. An int argument used to convert to float.
Now it matches no overload exactly, so the compiler reports FS0041. FromHours and
FromDays have int overloads and still compile.
| Code | Fix |
TimeSpan.FromSeconds 5 | TimeSpan.FromSeconds 5.0 |
TimeSpan.FromSeconds(pown 2 n) | TimeSpan.FromSeconds(float (pown 2 n)) |
let toSpan x = TimeSpan.FromSeconds x | let toSpan (x: float) = TimeSpan.FromSeconds x |
Convert the argument at the call. Do not change the type of a public parameter or a record field to fix FS0041. For other FS0041 errors, read the candidate list in the message and annotate the argument with one candidate type.
Verify
- Build with
NullableandTreatWarningsAsErrorson. Do not addNoWarnfor 3261. - Search the changed code for
nonNull,#nowarn,Some, andstringused as a function (string x,x |> string). Check each use on a .NET result against the tables above. - Run the code with null inputs at each boundary: a JSON
null, an unset environment variable, an exception with no inner exception, and an empty reader.
To check the facts in this skill, run the bundled proof:
pwsh ./scripts/run.ps1The runner builds scripts/Clean with nullness and warnings as errors,
runs it, and compares 19 results. It then builds each file in scripts/breaks
and checks that it fails with only the diagnostic codes in its // expect: lines and with the message text in its // expect-text: lines.
The clean lines it prints are the results in the tables above.
These rules come from the F# compiler documentation and are not bundled proofs:
<Nullable>enable</Nullable> passes --checknulls+, #nowarn "3261" and NoWarn
hide later nullness warnings, and an unannotated function infers T | null from its body.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

