A data type classifies values and lets the compiler enforce which values and operations fit together. In F#, types are inexpensive to create, so they can model business concepts rather than merely storage shapes. A City and a Country may both contain strings, yet distinct wrapper types can prevent them from being mixed accidentally. section 1.5.2
Types work especially well when an application has rules that must hold before an operation can proceed. Representing a validated value with a dedicated type moves checking to one clear boundary: code that sends email can require a ValidatedEmail instead of repeatedly asking whether an ordinary string is valid. section 14.1.1
type ValidatedEmail =
private | ValidatedEmail of string #1
member this.Value = match this with (ValidatedEmail e) -> e #2
static member Create (unvalidatedEmail:string) = #3
if unvalidatedEmail.Contains "@" then
Ok (ValidatedEmail unvalidatedEmail)
else
Error "Invalid email!"
let sendEmailSafe message (email:ValidatedEmail) = #4
...
...
...
Ok "Sent message"
let sendResult =
ValidatedEmail.Create "isaac@email.com"
|> Result.bind (sendEmailSafe "Welcome!") #5
ValidatedEmail has a private representation and a Create operation. sendEmailSafe accepts only the validated type, so callers must validate before sending. section 14.1.1
This approach narrows testing to the code that creates the validated value. Once a ValidatedEmail exists, sendEmailSafe can follow its happy path; the type prevents calls using an unvalidated email. Types can therefore encode prerequisites as well as describe data. section 14.1.1
Types also support a data-and-functions design:
- A function transforms data of one type into data of another type. section 1.5.5
- Functions such as validation, invoice creation, and email delivery can be composed into a pipeline. section 1.5.5
- Discriminated unions let the compiler check whether pattern matching handles every possible case. section 14.1.1
Guide type inference when an error spreads
F# infers types from usage. If calculateGroup compares age with the string "test", the compiler infers age as a string; later calls passing integers then fail too. Inspecting the inferred signature, such as string -> string, reveals the assumption. Add a temporary annotation that says age is an int to direct inference and place the error at the mistaken comparison. section 3.2.4
let calculateGroup age = #1
if age < 18 then "Child"
elif age < 65 then "Adult"
else "Pensioner"
let sayHello someValue = #2
let group = #3
if someValue < 10.0 then calculateGroup 15
else calculateGroup 35
"Hello " + group
let result = sayHello 10.5 #4
Use the small calculateGroup and sayHello script to observe how one changed literal alters an inferred input type and produces errors at call sites. section 3.2.4
Specific types are optional, not mandatory, but using them early makes domain rules visible in code and guards against incorrect data combinations. F# types are also intended to be usable from C#, with some caveats when crossing the language boundary. section 1.5.2 section 11.2

















