Skip to content

FSharpValue.GetRecordFields skips None values with anonymous records #3477

Closed

Description

@reinux

Description

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.

Related information

  • Fable version: `4.0.6
  • Operating system: Windows 11

Activity

  1. changed the title [-]FSharpValue.GetRecordFields skips `None` values[/-] [+]FSharpValue.GetRecordFields skips `None` values with anonymous records[/+] on Jun 16, 2023
  2. fable-repo-assist commented on Apr 26, 2026

    @fable-repo-assist
    Contributor

    🤖 This is an automated response from Repo Assist.

    I've traced this to the BabelPrinter in src/Fable.Transforms/BabelPrinter.fs at around line 1211:

    properties
    |> Array.filter (function
        | ObjectProperty(_, UndefinedOrVoid, _, _) -> false  // ← filters out undefined values
        | _ -> true)

    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:

    1. 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.
    2. Track omitted fields — instead of simply filtering, note which field indices are undefined so getRecordFields can reconstruct the full array.
    3. 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).

    Generated by 🌈 Repo Assist, see workflow run. Learn more.

    To install this agentic workflow, run

    gh aw add githubnext/agentics/workflows/repo-assist.md@51c8f6ad4357d2ecc06e47120031b3d75e80227d
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions