Write code that can't be wrong
Not because you wrote tests. Not because you wrapped everything in try/catch
.
But because the type of the function left no room for mistakes.
Let's go through some examples of types that make mistakes impossible.
function stringFn(x: string): string
Here's a very ordinary-looking function type:
function stringFn(x: string): string
This type tells you almost nothing about what the function does. Here are four completely different implementations that would all pass type checking:
x => x // identity
x => x + x // duplicate
x => x.trim() // transform
x => "hello" // constant
Because we know the type of the function input and output, we can implement this function to:
- Inspect or transform the input
- Replace it entirely
- Do anything that returns a
string
And the type checker will have nothing to say.
This type gives you no behavioral guarantees.
There are infinite valid implementations.
function id<A>(x: A): A
Now watch what happens when we introduce a generic:
function id<A>(x: A): A
Can you see how this is related to the last type we looked at? We just add a generic parameter A
to replace our prior use of string
.
This function knows nothing about A
. It can’t inspect it, manipulate it, or manufacture new values of it.
We're ruling out infinite recursion because the function type promises to return a value.
That means there’s only one thing a function with this type can do:
function id<A>(x: A): A {
return x
}
That's it.
If you try to do anything else - return a constant, log a property, even just call a method - it won't pass type checking.
Exactly one valid implementation.
The type alone enforces correctness.
Look at the impact of introducing that generic:
(x: string) => string |
<A>(x: A) => A |
---|---|
Infinite valid implementations | Only one valid implementation |
Can inspect or transform input | Cannot even look inside x |
No behavioral guarantees | Behavior guaranteed by type |
Adding a generic doesn’t limit what you can express.
It limits what can go wrong.
This enforced correctness that we get by introducing a generic is referred to as parametricity. We will continue to leverage parametricity in the next examples.
function choose<A>(x: A, y: A): A
Here's a more interesting type.
Now we take two inputs, both of the same generic type:
function choose<A>(x: A, y: A): A
We still know nothing about A
. That means we still can’t:
- Inspect
x
ory
- Compare them
- Construct new values
We can only return one of the two.
(cond, x, y) => x
(cond, x, y) => y
Total valid implementations: 2
Parametricity again constrains behavior. We can count the number of valid functions on one hand.
function ifThen<A>(cond: boolean, x: A, y: A): A
Let’s add a known type back into the mix: a boolean flag.
function ifThen<A>(cond: boolean, x: A, y: A): A
What are our options?
cond
has 2 possible values:true
andfalse
- For each case, we can return either
x
ory
That gives us exactly 4 possible implementations:
(cond, x, y) => x // always x
(cond, x, y) => y // always y
(cond, x, y) => cond ? x : y // normal conditional
(cond, x, y) => cond ? y : x // inverted conditional
Total valid implementations: 4
Parametricity rules out everything else.
function apply<A, B>(f: (a: A) => B, x: A): B
This is the classic function application form:
function apply<A, B>(f: (a: A) => B, x: A): B
We’re given:
- A function
f
fromA
toB
- A value
x
of typeA
We must return a B
. Since we:
- Know nothing about
A
orB
- Can’t construct or inspect either
The only thing we can do is call f(x)
:
function apply<A, B>(f: (a: A) => B, x: A): B {
return f(x)
}
Total valid implementations: 1
This is what it means to be correct by construction. The type makes mistakes impossible.
With the first type we looked at, we saw that with concrete types, we can do whatever we want. Our implementation space is wide open.
When we start introducing generic types, we have fewer and fewer options for how to implement a function signature. Our implementation space gets narrow and correctness becomes automatic.
This is the power of parametricity: it tells you what your code must do, simply by how the types are written.
Next time you write a function, ask:
- Can I make this generic?
- What does the type allow—and forbid?
- Can I design it so there's only one way to get it right?
This isn't limited to cherry picked examples.
In upcoming posts, we'll look at how to introduce generics to messy "real world" code.
Get 1:1 Mentoring (and Shape my next book)
I’m offering free 1:1 mentoring to a small group of devs who want to put these ideas to work in real code—refactoring, modeling, and building with type safety by design.
You’ll get:
- Hands-on help applying parametric reasoning in your own projects
- Early access to material from the book I’m writing
- A chance to shape the book’s direction by showing me what really matters
Interested?
Fill out this short form and I’ll be in touch.