Records
This file covers record creation and flat property access on fully-known records. For row-polymorphic access through lambda parameters, see Rows.fs.
record creation from literals
Inferred
Source
{
age = 22
name = "John"
address = "123 Main St"
}
Inferred
{ age: Number; name: String; address: String }
The most direct way to produce a record: build it inline from literals. The result type is a structural record — one field per definition, each carrying the type of its value. Notably, no record name was declared anywhere; the type is inferred purely from the shape. This is the headline feature demonstrated at its simplest.
F# test body
X.MkRecord [
X.Field "age" (X.Lit 22)
X.Field "name" (X.Lit "John")
X.Field "address" (X.Lit "123 Main St")
]
|> solve [] None
|> shouldSolveType (
Mono (TDef.NamedRecordWith (NameHint.Given "Person") [
"age", BuiltinTypes.number
"name", BuiltinTypes.string
"address", BuiltinTypes.string
]))
property access on environment-provided record
Inferred
Env
add : Number -> Number -> Number
order : { quantity: Number }
Source
add 10 order.quantity
Property access on a record bound in the environment.
F# test body
X.App (X.App (X.Var "add") (X.Lit 10)) (X.PropAcc (X.Var "order") "quantity")
|> solve
[
yield! envWithArithmetic
yield "order", Mono (TDef.NamedRecordWith (NameHint.Given "Order") [
"quantity", BuiltinTypes.number
])
]
None
|> shouldSolveType (Mono BuiltinTypes.number)
let-bound record keeps its fields
Inferred
Source
let myRecord =
{
age = 22
name = "John"
}
myRecord
Inferred
{ age: Number; name: String }
Round-trips a record through a let binding and returns it unchanged. The test confirms that let doesn't lose information — the outer expression's inferred type is exactly the record's type, fields and all. A type checker that "forgot" fields at a binding boundary would be visibly broken here.
F# test body
X.Let (X.Ident "myRecord") (
X.MkRecord [
X.Field "age" (X.Lit 22)
X.Field "name" (X.Lit "John")
]
) (X.Var "myRecord")
|> solve [] None
|> shouldSolveType (
Mono (TDef.NamedRecordWith (NameHint.Given "Person") [
"age", BuiltinTypes.number
"name", BuiltinTypes.string
]))
property access on a let-bound record
Inferred
Source
let myRecord = { age = 22; name = "John" } in
myRecord.name
Combines let with .-field access — both covered elsewhere in isolation — and checks the two interact correctly. After binding the record, reading name should yield that field's type. This is the everyday shape of record usage, so it better just work.
F# test body
X.Let (X.Ident "myRecord") (
X.MkRecord [
X.Field "age" (X.Lit 22)
X.Field "name" (X.Lit "John")
]
) (
X.PropAcc (X.Var "myRecord") "name"
)
|> solve [] None
|> shouldSolveType (Mono BuiltinTypes.string)
property access on a let-bound record (2)
Inferred
Source
let myRecord = { age = 22; name = "John" } in
myRecord.age
Same expression, different field. Together with the previous test this confirms the inferencer returns the right field type per access, not just the first one it finds. It's easy to write a naïve implementation that always gets one access correct and quietly confuses the other.
F# test body
X.Let (X.Ident "myRecord") (
X.MkRecord [
X.Field "age" (X.Lit 22)
X.Field "name" (X.Lit "John")
]
) (
X.PropAcc (X.Var "myRecord") "age"
)
|> solve [] None
|> shouldSolveType (Mono BuiltinTypes.number)
accessing a non-existing field fails
Expected error
Source
let myRecord = { age = 22; name = "John" } in
myRecord.xxxxxxxx
Error
Member 'xxxxxxxx' is missing in record type { age: Number; name: String }
Asking for a field that isn't there must be a static error. The record's full shape is known at this point, so a missing-member problem can (and should) be caught before the program runs. Dynamic languages surface this as a runtime crash — TypeFighter refuses to typecheck the program at all.
F# test body
X.Let (X.Ident "myRecord") (
X.MkRecord [
X.Field "age" (X.Lit 22)
X.Field "name" (X.Lit "John")
]
) (
X.PropAcc (X.Var "myRecord") "xxxxxxxx"
)
|> solve [] None
|> shouldFail
comparing two differently-typed fields fails
Expected error
Env
EQUALS : forall a. a -> a -> Bool
Source
let r = { IntField = 3; BooleanField = true } in
EQUALS r.BooleanField r.IntField
Error
Can't unify Bool and Number
Both arguments to EQUALS must be the same type, but the accessed fields disagree.
F# test body
let env =
[ "EQUALS", TDef.Generalize (%1 ^-> %1 ^-> BuiltinTypes.boolean) ]
X.Let
(X.Ident "r")
(X.MkRecord [
X.Field "IntField" (X.Lit 3)
X.Field "BooleanField" (X.Lit true)
])
(X.App
(X.App (X.Var("EQUALS")) (X.PropAcc (X.Var "r") "BooleanField"))
(X.PropAcc (X.Var "r") "IntField"))
|> solve env None
|> shouldFail
using multiple fields in a boolean expression
Inferred
Env
AND : Bool -> Bool -> Bool
EQUALS : forall a. a -> a -> Bool
Source
let r = { IntField = 3; BooleanField = true } in
AND (EQUALS r.IntField 3) (EQUALS r.BooleanField true)
Accessing multiple fields, each compared against a matching literal.
F# test body
let env =
[
"AND", Mono(BuiltinTypes.boolean ^-> BuiltinTypes.boolean ^-> BuiltinTypes.boolean)
"EQUALS", TDef.Generalize (%1 ^-> %1 ^-> BuiltinTypes.boolean)
]
let left =
X.App
(X.App (X.Var("EQUALS")) (X.PropAcc (X.Var "r") "IntField"))
(X.Lit 3)
let right =
X.App
(X.App (X.Var("EQUALS")) (X.PropAcc (X.Var "r") "BooleanField"))
(X.Lit true)
X.Let
(X.Ident "r")
(X.MkRecord [
X.Field "IntField" (X.Lit 3)
X.Field "BooleanField" (X.Lit true)
])
(X.App
(X.App (X.Var "AND") left)
right)
|> solve env None
|> shouldSolveType (Mono(BuiltinTypes.boolean))