Nullness issue - type check in multi-match expressions doesn't eliminate null
Issue description
I expect that type check in multi-match expressions eliminates nulls after check just like in simple match cases.
Choose one or more from the following categories of impact
- [x] Unexpected nullness warning (false positive in nullness checking, code uses --checknulls and langversion:preview).
- [ ] Missing nullness warning in a case which can produce nulls (false negative, code uses --checknulls and langversion:preview).
- [ ] Breaking change related to older
nullconstructs in code not using the checknulls switch. - [ ] Breaking change related to generic code and explicit type constraints (
null,not null). - [x] Type inference issue (i.e. code worked without type annotations before, and applying the --checknulls enforces type annotations).
- [ ] C#/F# interop issue related to nullness metadata.
- [ ] Other (none of the categories above apply).
Operating System
Windows (Default)
What .NET runtime/SDK kind are you seeing the issue on
.NET SDK (.NET Core, .NET 5+)
.NET Runtime/SDK version
.NET SDK 10.0.100-rc.2.25502.107
Reproducible code snippet and actual behavior
let test (s: string) =
()
[<EntryPoint>]
let main argv =
let x = true
let y: string | null = ""
match x, y with
| true, _ -> ()
| false, null -> ()
| false, s -> test s
0
s type is mistakenly resolved as string | null
Possible workarounds
let test (s: string) =
()
[<EntryPoint>]
let main argv =
let x = true
let y: string | null = ""
match x with
| true -> ()
| false, Null -> ()
| false, NonNull s -> test s
0
The Null / NonNull active pattern from FSharp.Core is solving that and is the way to go for non-trivial matches over null data, also works with nested fields and everywhere you would expect.
@T-Gro thanks, however I think it is expected that for tuples regular null checks should work. It's also promised in the release post:
The same technique works for matching tuples of arguments, with keeping track about handled null for each of the elements.
I see where you are coming from.
In your example, this is obvious since true as well as false are handled, and we know there can't be more values.
In the general case, this needs to align with exhaustiveness logic and this is why the active pattern is in place, since active patterns already do that.
The existing behavior is:
// In a tuple of size N, if 1 elem is matched for null and N-1 are wild => subsequent clauses can strip nullness
Yes, I understand that there is a workaround with active patterns (updated the description). The question is - should the tuple match limitation be fixed or mentioned in documentation? I feel that ideally it should be fixed (since developer really expects that to work just like for single value case), but if the fix is too complicated, then at least the behavior should be documented.
You are correct, the active pattern is a technical solution that avoided a complex implementation and was also risk free from a breaking perspective (since the AP was new).
We would definitely accept a contribution aspiring to align that with plain ´null ´ pattern.
Docs should get an update for the time being , agree
Od: Vladimir Shchur @.> Odesláno: středa 5. listopadu 2025 16:27 Komu: dotnet/fsharp @.> Kopie: Comment @.>; Assign @.>; Subscribed @.***> Předmět: Re: [dotnet/fsharp] Nullness issue - type check in multi-match expressions doesn't eliminate null (Issue #19042)
Yes, I understand that there is a workaround with active patterns (updated the description). The question is - should the tuple match limitation be fixed or mentioned in documentation? I feel that ideally it should be fixed (since developer really expects that to work just like for single value case), but if the fix is too complicated, then at least the behavior should be documented.
— Reply to this email directly, view it on GitHubhttps://github.com/dotnet/fsharp/issues/19042#issuecomment-3491890816 or unsubscribehttps://github.com/notifications/unsubscribe-auth/ALDDFX2ELZSVC2LEKXPRU2L33IJNVBFKMF2HI4TJMJ2XIZLTS6BKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDUOJ2WLJDOMFWWLLTXMF2GG2C7MFRXI2LWNF2HTAVFOZQWY5LFUVUXG43VMWSG4YLNMWVXI2DSMVQWIX3UPFYGLAVFOZQWY5LFU4ZTGMRZGYYDNJDOMFWWLKDBMN2G64S7NFSIFJLWMFWHKZNJGE3DINZYHE3DANVENZQW2ZNJNBQXGX3MMFRGK3ECUV3GC3DVMWVDINBVHEYTOMBTGQ4KI3TBNVS2S2DBONPWYYLCMVWIFJLWMFWHKZNKG4YTGNBVGQYDSMJQURXGC3LFVFUGC427NRQWEZLMVRZXKYTKMVRXIX3UPFYGLLCJONZXKZKDN5WW2ZLOOSTHI33QNFRXHEUCUR2HS4DFVJZGK4DPONUXI33SPGSXMYLMOVS2QMRZGA2DQOBZGGBKI5DZOBS2K2LTON2WLJLWMFWHKZNKGM2TONRQG4YDQNJUU52HE2LHM5SXFJTDOJSWC5DF. You are receiving this email because you commented on the thread.
Triage notifications on the go with GitHub Mobile for iOShttps://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Androidhttps://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.
Matching is the way to go for NRTs as it is for options. The type of a pattern-matched binding s should not depend on previous patterns and the fact that it does so with | s -> depending on whether there was a null match before or not is very unfortunate and this behavior shouldn't be extended. It's an import from C# where these hacks are needed.