Type-safe JavaScript Development with F# and Fable
F# is a suitable alternative to TypeScript for writing a JavaScript application with type-safety. But you also get the additional benefits of F#'s powerful type inference.
TypeScript
interface Person {
firstName: string
lastName: string
}
let people: Person[] = [
{ firstName: 'John', lastName: 'Doe' },
{ firstName: 'Jane', lastName: 'Doe' },
]
let names = people.map(({ firstName, lastName }) => `${firstName} ${lastName}`)Here is a simple TypeScript example where an array of Person is mapped to strings. In the inline mapping function, the parameter { firstName, lastName} does not need to have explicit type annotation because TypeScript can infer the type from the fact that it is mapping over an array of type Person[], therefore the type at each index must be Person.
TypeScript's type inference is not able to do the same if we extract the callback into a named function or a variable though.
interface Person {
firstName: string;
lastName: string;
}
let people: Person[] = [
{ firstName: 'John', lastName: 'Doe' },
{ firstName: 'Jane', lastName: 'Doe' },
]
let combineNames = ({ firstName, lastName }) => `${firstName} ${lastName}`
let names = people.map(combineNames)In this refactor, we'll now get a TypeScript eror (at least in strict mode) that the type of { firstName, lastName } is implicitly any. To handle that, an annotation can be added to the combineNames function like this:
let combineNames: (p: Person) => string = ({ firstName, lastName }) => `${firstName} ${lastName}`F#
F# has a mature and popular compiler called Fable which can compile type-safe F# code to JavaScript. Here's the same code in F#:
type Person = {
firstName: string
lastName: string }
let people = [
{ firstName = "John"; lastName = "Doe" }
{ firstName = "Jane"; lastName = "Doe" } ]
let names =
people
|> List.map (fun p -> $"{p.firstName} {p.lastName}")In this example, there is no type annotation on the people list. This is because F# knows that that type in the list matches the type Person and infers that is is a Person list. Changing one of the field names on one of the entries, removing a field, or adding a field will result in a compilation error.
We can also pull out the function we're using for List.map into a named binding and it's type will be inferred too:
type Person = {
firstName: string
lastName: string }
let people = [
{ firstName = "John"; lastName = "Doe" }
{ firstName = "Jane"; lastName = "Doe" } ]
let combineNames person =
$"{person.firstName} {person.lastName}"
let names =
people
|> List.map combineNamesIf you recall, in addition to having to annotate people in TypeScript, it also lost its ability to infer the type in the map function once it was given a name. Here all of this code is understood to be working on Person.