You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
FSharpValue.GetRecordFields skips None values with anonymous records #3477
GetRecordFields skips any field that is an Option.None, and returns a shorter array.
Update: Turns out this only affects anonymous records, so the situation isn't as severe, though I think it can lead to some confusing situations. Maybe the compiler emits objects that skip anonymous records initialized as None as opposed to making them null?
Repro code
Microsoft.FSharp.Core.Reflection.FSharpValue.GetRecordFields {| a = 3; b = (None: int option); c = Some 89 |}
Returns [| 3; 89 |].
FSharpValue.GetTupleFields behaves correctly against a similar tuple, e.g. (3; None; Some 89).
Expected and actual results
Value returned should be [| 3; undefined; 89 |] . FSI returns [| 3; null; Some 89 |].
Workaround
Use FSharpType.GetRecordFields to first get the PropertyInfos for the fields, and then map that array against FSharpValue.GetRecordField values individually.
changed the title [-]FSharpValue.GetRecordFields skips `None` values[/-][+]FSharpValue.GetRecordFields skips `None` values with anonymous records[/+]on Jun 16, 2023
When an anonymous record like {| b = (None: int option) |} is compiled, the None field becomes undefined (since mustWrapOption int = false). The BabelPrinter then filters out the property entirely from the object literal, so the emitted JS is { a: 3, c: 89 } without b at all.
Since FSharpValue.GetRecordFields in Reflection.ts uses Object.keys(v).map(k => v[k]), it only sees keys that actually exist in the object — so the b field is simply absent from the result.
This filter exists to avoid emitting noisy b: undefined in the output, which is a reasonable optimisation for TypeScript (where b?: int and b: int | undefined are interchangeable). However, it breaks reflection.
Potential fixes:
Pass type info to getRecordFields — augment the call in Replacements.fs to also pass the anonymous record's type info, then use it in Reflection.ts to enumerate fields in the correct order/count.
Track omitted fields — instead of simply filtering, note which field indices are undefined so getRecordFields can reconstruct the full array.
Don't filter in anonymous record construction — special-case anonymous records to always emit all keys (even if undefined), at the cost of slightly noisier JS output.
Option 1 is the cleanest but requires threading type info through. Option 3 is the simplest change.
Note: this likely affects regular records too when their fields are None, unless regular records are compiled as class instances rather than plain object literals (which would explain why the author found it only affects anonymous records).
Activity
[-]FSharpValue.GetRecordFields skips `None` values[/-][+]FSharpValue.GetRecordFields skips `None` values with anonymous records[/+]fable-repo-assist commentedon Apr 26, 2026
🤖 This is an automated response from Repo Assist.
I've traced this to the BabelPrinter in
src/Fable.Transforms/BabelPrinter.fsat around line 1211:When an anonymous record like
{| b = (None: int option) |}is compiled, theNonefield becomesundefined(sincemustWrapOption int = false). The BabelPrinter then filters out the property entirely from the object literal, so the emitted JS is{ a: 3, c: 89 }withoutbat all.Since
FSharpValue.GetRecordFieldsin Reflection.ts usesObject.keys(v).map(k => v[k]), it only sees keys that actually exist in the object — so thebfield is simply absent from the result.This filter exists to avoid emitting noisy
b: undefinedin the output, which is a reasonable optimisation for TypeScript (whereb?: intandb: int | undefinedare interchangeable). However, it breaks reflection.Potential fixes:
getRecordFields— augment the call inReplacements.fsto also pass the anonymous record's type info, then use it inReflection.tsto enumerate fields in the correct order/count.getRecordFieldscan reconstruct the full array.undefined), at the cost of slightly noisier JS output.Option 1 is the cleanest but requires threading type info through. Option 3 is the simplest change.
Note: this likely affects regular records too when their fields are
None, unless regular records are compiled as class instances rather than plain object literals (which would explain why the author found it only affects anonymous records).