Svix is the enterprise ready webhooks sending service. With Svix, you can build a secure,
reliable, and scalable webhook platform in minutes. Looking to send webhooks?
Give it a try!
Edit: previous version of this post didn’t have “static” in the title to keep it short (but the body did). Added it in the title for clarification.
I’ve been writing software for over 20 years, and with every day that goes by I grow more certain that strong static typing is not just a good idea, but also almost always the right choice.
There are definitely uses for untyped languages (or language variants), for example they are much nicer when using a REPL, or for throwaway scripts in environments that are already hopelessly untyped (e.g. the shell). In almost every other case, however, strong typing is strongly preferred.
There are advantages to not using types, such as a faster development speed, but they pale in comparison to all of the advantages. To that I say:
Writing software without types lets you go at full speed. Full speed towards the cliff.
The question around strong static typing is simple: would you rather work a bit more and get invariants checked at compile-time (or type-checking time for non-compiled languages), or work a bit less and have them be enforced at runtime, or even worse not enforced even at runtime (JavaScript, I’m looking at you… 1 + "2" == 12
).
Getting errors at runtime is a terrible idea. First, it means that you won’t always catch them during development. Second, when you do catch them, it will happen in a customer facing manner. Yes, tests help, but writing tests for every possible mistyped function parameter is impossible given the endless possibilities. Even if you could, having types is much easier than testing for wrong types.
Types lead to less bugs
Types also offer annotations to code that benefit both humans and machines. Having types is a way to more strictly define the contract between different pieces of code.
Consider the following four examples. They all do exactly the same thing just with varying level of contract definition.
function birthdayGreeting1(...params) {
return `${params[0]} is ${params[1]}!`;
}
function birthdayGreeting2(name, age) {
return `${name} is ${age}!`;
}
function birthdayGreeting3(name: string, age: number): string {
return `${name} is ${age}!`;
}
The first one doesn’t even define the number of parameters, so it’s hard to know what it does without reading the docs. I believe most people will agree the first one is an abomination and wouldn’t write code like that. Though it’s a very similar idea to typing, it’s about defining the contract between the caller and the callee.
As for the second and the third, because of the typing, the third will need less documentation. The code is simpler, but admittedly, the advantages are fairly limited. Well, until you actually change this function…
In both the second and the third functions, the author assumes the age is a number. So it is absolutely fine to change the code as below:
function birthdayGreeting2(name, age) {
return `${name} will turn ${age + 1} next year!`;
}
function birthdayGreeting3(name: string, age: number): string {
return `${name} will turn ${age + 1} next year!`;
}
The problem is that some of the places that use this code accept user input which was collected from an HTML input (so always a string). Which will result in:
> birthdayGreeting2("John", "20")
"John will turn 201 next year!"
While the typed version will correctly fail to compile because this function excepts age to be a number, not a string.
Having the contract between a caller and callee is important for a codebase, so that callers can know when callees change. This is especially important for an open source library, where the callers and the callees are not written by the same group of people. With this contract it’s impossible to know how things change when they do.
Types lead to a better development experience
Typing can also be used by IDEs and other development tools to vastly improve the development experience. You get notified as you code if any of your expectations are wrong. This significantly reduces cognitive load. You no longer need to remember the types of all the variables and the function in the context. The compiler will be there with you and tell you when something is wrong.
This also leads to a very nice additional benefit: easier refactoring. You can trust the compiler to let you know whether a change you make (e.g. the change in our example above) will break assumptions made elsewhere in the code or not.
Types also make it much easier to onboard new engineers to a codebase or library:
- They can follow the type definitions to understand where things are used.
- It’s much easier to tinker with things as changes will trigger a compile error.
Let’s consider the following changes to our above code:
class Person {
name: string;
age: number;
}
function birthdayGreeting2(person) {
return `${person.name} will turn ${person.age + 1} next year!`;
}
function birthdayGreeting3(person: Person): string {
return `${person.name} will turn ${person.age + 1} next year!`;
}
function main() {
const person: Person = { name: "Hello", age: 12 };
birthdayGreeting2(person);
birthdayGreeting3(person);
}
It’s very easy to see (or use your IDE to find) all the places where Person
is used. You can see it’s initiated in main
and you can see it’s used by birthdayGreeting3. However, in order to know it's used in
birthdayGreeting2`, you’d need to read the entire codebase.
The flip side of this is also that when looking at birthdayGreeting2
, it’s hard to know that it expects a Person
as a parameter. Some of these things can be solved by exhaustive documentation, but: (1) why bother if you can achieve more with types? (2) documentation goes stale, here the code is the documentation.
It’s very similar to how you wouldn’t write code like:
function birthdayGreeting2(a) {
b = person.name;
c = person.age;
return `${b} will turn ${c + 1} next year!`;
}
You would want to use useful variable names. Typing is the same, it’s just variable names on steriods.