newsletter

How (and why) I became obsessed with type safety

You can make your applications unbreakable using types. This is how I went from PHP and dynamic languages to learning types and functional programming.


Sandro Maglione

Sandro Maglione

Software development

I am a strong advocate of types. Like strong 🤘

The combination of a powerful type system and the knowledge of how to use it makes for an unbreakable codebase. Like unbreakable, for real.

It requires some mental gymnastics to get used to it, but once you get this you'll never look back.

Here is some tricks and ideas I learned over the years 🛠️


Why do I care

I started my dev journey with PHP. PHP 🐘

As far removed from types as you can possibly be (alongside Python of course 🐍).

No types is like "No man's land": as long as something works, don't touch it. No structure, no "design pattern", no "programming paradigm".

I first noticed the advantage of types moving from javascript to typescript. Suddenly programming was not anymore like trowing darts, but everything was required to become structured.

Types: constraints that you apply for yourself on how something should be used.

That's because we are humans, we forget fast, but we want to maintain a production codebase for years 🫡

So instead of javascripty/trial-and-error programming:

function getContent({ slug }) {
  return `articles/${slug}`;
}

We want strict checking "I need to pass this exact value" programming:

type ValidSlug = string & Brand.Brand<"ValidSlug">;
type ContentFolder = "articles" | "newsletter";

const getContent = ({ slug }: { readonly slug: ValidSlug }): `${ContentFolder}/${ValidSlug}` =>
  `articles/${slug}`;

Same code, different constraints 👆

I never came back to untyped languages 🫡

Types hang out with functions (less so with classes)

Second step has been moving away from Object Oriented and closer to Functional.

Turns out OOP patterns can be implemented with functions:

From "Functional programming design patterns by Scott Wlaschin": every design pattern in OOP is a function.From "Functional programming design patterns by Scott Wlaschin": every design pattern in OOP is a function.

This image is from "Functional programming design patterns by Scott Wlaschin" 👀

From a constellation of class like this:

abstract class IAlgorithm {
  abstract doSomething(): void;
}

class Impl1 implements IAlgorithm {
  doSomething(): void {
    console.log("It's 1"!);
  }
}

class Impl2 implements IAlgorithm {
  doSomething(): void {
    console.log("It's 2"!);
  }
}

const program = (algorithm: IAlgorithm) => {
  algorithm.doSomething();
};

const main = () => {
  program(new Impl1());
};

To shrinking down all to a single type with functions:

interface Algorithm {
  readonly doSomething: () => void;
}

const program = (algorithm: Algorithm) => {
  algorithm.doSomething();
};

const main = () => {
  program({
    doSomething: () => console.log("Did it!"),
  });
};

Doing this allows more control, less code, and better composition (no inheritance please!).

Type the full architecture

My (and the ecosystem) progress brings us to today.

I work mostly with Typescript these days. For more than a couple of reasons:

  • Types are powerful: unions, tuples, template literals, branded types
  • Less OOP, more focus on functions
  • Type inference 🪄
  • Developer experience (IDE integration, speed of development, dev tools)

All these features put together can make a Functional Effect System.

Instead of typing only parameters and return types, we can type the full architecture:

export class Algorithm extends Effect.Tag("Algorithm")<
  Algorithm,
  { readonly doSomething: () => void }
>() {}

export class Request extends Effect.Tag("Request")<
  Request,
  { readonly makeRequest: Effect.Effect<string, UnknownException> }
>() {}

const main: Effect.Effect<string, UnknownException, Algorithm | Request> =
  Effect.gen(function* () {
    yield* Algorithm.doSomething();
    return yield* Request.makeRequest;
  });

Everything is type-driven. Notice how we did not define any concrete implementation.

That's because (in a sense) it doesn't matter 💁🏼‍♂️

The types are responsible to enforce our intention. Everything then composes together, and the types propagate:

  • main calls doSomething from Algorithm and makeRequest from Request, therefore both Algorithm and Request are present in the final type signature (Algorithm | Request)
  • Calling makeRequest can fail with UnknownException, therefore UnknownException is present in the type
  • makeRequest returns string which is returned from main

Everything is inferred by the type system, no need of manual typing!

When you get to this level of composition and type safety there is really no come back 🫡


This is the direction I am going. Everything I write is an experiment for full type safety.

I am working on an effect course about how to make your architecture type safe.

After that many more courses (all free) are coming on Typeonce.dev. All will be focused on how to get the most out of types with different libraries (XState, Tanstack Query, Hono are on my radar 📡).


I just released a new update (refactoring) of sandromaglione.com, it's now all a static export of nextjs.

Meanwhile I am sprinting to release my first course on effect on typeonce.dev.

You can also check out some snippets I released (copy-paste ready 👌).

A lot in progress 🚀

See you next 👋

Start here.

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.