United Kingdom: +44 (0)208 088 8978

How I Went from TypeScript to F#

After working with TypeScript a lot, Amir went looking for an alternative. In this blog post, he shares why he moved from TypeScript to the wonderful world of F#

We're hiring Software Developers

Click here to find out more

Prior to joining CIT, I was well-versed in the JavaScript ecosystem. I used tools like React, Node.js, Vite, and all the countless projects that exist and continue to be introduced. Building things in JavaScript seemed well-supported. After all, it is one of the most popular ecosystems given it is the language of the browser—and since I build most of what I create in the browser, it had a natural appeal. However, it unfortunately didn’t align with the philosophy I have towards building software systems, whether those are web apps, distributed systems, APIs, or more! After a number of years, I began to notice flaws in the design of JavaScript and TypeScript as a whole that made me want to search for alternative ways of building software that seemed timeless.

It was a grueling process, but I thoroughly enjoyed it at the same time. I want to briefly explain some of the main drawbacks that TypeScript had that prompted me to search elsewhere.

Firstly: TypeScript is not strongly typed.

Why is this important to me?

Because I value not shooting myself in the foot. We are humans. We make mistakes. Building software and especially maintaining it is not an easy task, especially as a system grows in complexity with different domain knowledge scattered across it. The last thing you want is to wake up at 3 a.m. to alerts from your hosted service with a bunch of HTTP 500 errors and customer complaints because a new feature you shipped broke something somewhere else in the code you weren’t made aware of. This can be costly. This can be demotivating. And most importantly, this feedback loop can make software decay over time. It will become harder.

Humans are influenced by their environment. If you cultivate the right environment, then you inherently bring out better decisions within the constraints that the environment gives.

Let’s start with null—the billion-dollar mistake. In TypeScript, I can specify “strict” null checks, which basically means that the TypeScript compiler will inform me that I need to handle the null case; however, it won’t enforce it.

{
  "compilerOptions": {
    // ...
    "strictNullChecks": true,
    // ...
  }
}

Why does it not enforce it? Because in TypeScript, null handling often leads to taking shortcuts. Unlike F# where Option types are the idiomatic choice and null exists mainly for .NET interop, TypeScript's ecosystem is deeply intertwined with null. In TypeScript, we can simply do a non-null assertion (!):

let value: string | null = "some string";

// ... some operations that may change the value > variable

// Who cares what the value is now, just give it to > me :)
someFunction(value!);

Or we could just bypass the type system entirely with any:

let value: any = "some string";

// ... some operations that may change the value variable

someFunction(value);

While responsible developers should implement proper defensive programming patterns, TypeScript's flexibility here is a double-edged sword. The language allows you to "go off the rails" if you choose to. And since we naturally gravitate towards the path of least resistance, it's tempting to reach for ! or any as quick fixes - something I've done myself, only to have it backfire later. This differs significantly from F#'s approach, where the language actively guides you towards using Option types and makes null handling explicit and intentional, promoting better practices by design.

Okay, so what if we tried to not use null at all? Instead, we go for an Option type. Well… unfortunately, TypeScript doesn’t have built-in support for this, so you have to create your own… but even that is not fully null-free.

type Option<T> = T | null | undefined; // Null still creeps in here

F#, on the other hand, has a built in Option type, making it less tempting to pull out null

type Option<'T> = 
  | Some of 'T 
  | None

On top of that, in F#, it’s much easier to pattern match and break out how each case should be handled, as pattern matching is built-in—similar to languages like Rust, Elm, or Haskell.

match getName id with
| Some name -> printfn "Hello, %s!" name
| None -> printfn "No name found"

What about modeling the domain with the type system?

Well, in TypeScript you can use unions; however, they have weaker compile-time guarantees and are less rigorous.

type EmailType = ValidatedEmail | UnvalidatedEmail;

You can’t store data against each case, and you can’t pattern match on the union.

You have to hack around it by providing a tag or type field for each case:

type ValidatedEmail = {
  value: string;
  tag: "validated";
}
type UnvalidatedEmail = {
  value: string;
  tag: "unvalidated";
}
type EmailType = ValidatedEmail | UnvalidatedEmail;

This is more code just to get something implemented. F# is much more expressive:

type Email = 
  | Validated of string
  | Unvalidated of string

Another pain point is uncaught exceptions in the browser. In JavaScript and TypeScript, it’s easy to write code that throws exceptions at runtime, especially if you’re not careful with error handling. Uncaught exceptions can lead to a poor user experience, as they might crash the application or lead to inconsistent states. In a browser environment, this can be particularly problematic because users might not know what’s going on, and debugging can become a nightmare.

You see, TypeScript has a type system, and it works well enough, but it’s too weak to prevent you from building timeless systems. You can reach for community-built solutions like ts-pattern, but adding more dependencies can also make you lose control over how your application runs, as it is dependent on those dependencies working as expected. Why not just use a language that has it all built-in?

If it is too loose like TypeScript, we will tend to stray towards making mistakes as we are human. However, an environment which is more constrained allows us to think more creatively about solving a problem and not reach for the easy, quick way which may shoot us in the foot later on.

My idea with moving over to F# was to satisfy my need for building software that stood the test of time. It's not like you can't do that in TypeScript, but it’s much easier in F#, and a much more pleasant experience.

To conclude, I love F# because it enables me to be an efficient developer, and by that I mean I can focus 100% on the business logic of what the system should do rather than the intricacies of a language and having to mentally put budget aside for edge cases that the language may surprise me with. I personally am willing to compensate some of the flexibility of moving fast with JavaScript/TypeScript with the safety that F# brings me, as it will enforce me to write code early on that makes it easier to grok once more when I revisit the same logic some time down the line.