Skip to main content

Command Palette

Search for a command to run...

The Convenience of a Type System

Updated
6 min read
The Convenience of a Type System

Context: Exploring the usefulness of type systems in the development of software. This is not a programming languages deep dive, so terminology might be custom-fitted for this discussion.

I do not try to define terms in the absolute, but I aim to explore what type systems give us as developers, and the difference between various kinds. Trying to pinpoint why it is useful, when and how.

What is a Type System?

Type systems are the mathematics and rules used mostly by programming languages to check the simple statement which is also a type check:

"Is A assignable to B?" - Where A and B are any types.

Runtime vs. Compile Time

When writing code, we use text written in a programming language. The machine can't really do anything with that, so we use a compiler to make it "machine understandable". This machine code is our binary which we execute and create a runtime environment for our program to run in.

Narrowing down on this flow, we can draw the following develop-run cycle

source code -> compilation -> code gen: binary -> execution: runtime

There are two popular places where we can place type checks: "compilation" and "runtime". In each check point we can also decide how to react when this check fails. We could decide that this check is not so important, and we can continue, or we must stop the cycle and report to the developer.

Runtime failure

Starting from the easy check point. If a type check fails in an already executing application, the system breaks. This might be recoverable, but any "Is A assignable to B?" check that fails on runtime, must be handled or the program cannot continue running.

Compile time failure

Failure of the type-check in compilation requires a bit more of our analysis.

Compilation phase is running on a developer machine, which is usually strong. It also running in an environment which is usually without any resource regime. This privilege allow us to run far better checks.

Some languages are "strongly typed" and we consider them compiled languages. In these languages, if we fail the type-check at compile time, we must stop and we cannot generate a binary. This for example happens in the following Java snippet

String x = "hello";
int y = (int) x; // Casting which the compiler can prove is wrong, so it will fail here

In Java's case, the type checks the compiler is doing, is a requirement to finish the compilation step. The consistent type information that the compiler check for and verify, is a critical piece in the puzzle which the compiler is trying to solve in order generate the binary.

Contrary to that, there are languages where the compilation phase doe not require the type checks to pass. For example - Typescript adds a type system on top of it's target language, Javascript. The runtime type system of Javascript is decoupled (but aligned) with Typescript's. This makes the type-check a non-requirement for the compilation phase, and even when we fail, we can still continue with the dev-run cycle and generate a "binary".

Take for example the following snippet in Typescript, that when we type check it, it will fail

let x:string = 5

In this case, our compiler will complain that "type 'number' is not assignable to type 'string'". We can still generate the target Javascript code for it, and it will be valid javascript which will be evaluated successfully.

let x = 5

Stripping the type information makes this a valid javascript, so Typescript in compilation phase just need to strip type information, and then we can continue to the runtime phase with no issue.

We can summarize the 3 modes of type systems:

  1. Compile Time - A strongly typed language which has a type system in runtime and the compiler must make sure we pass type check at compile time.
  2. Only Runtime - There is no check at compile time, types exist only in compile time, and they are implemented as checks in the runtime itself.
  3. Optional Compile Time - Usually on top of Runtime system, we overlay another type system at compile time, that can be used to check the program.

Type Checks as quality feedback

Compile time type system, optional or not, are best looked at as cheap tests. They are very quick to add, clear to the reader and easy to reason about. These tests are so cheap, that you do not need to run a test runner to test them, the IDE (development environment) will check them for you in the background as you type.

In optional systems, it is even more so - The optionality of the checks allow us to play and prototype rapidly with or without types, break them and still execute the program. We can incrementally add type, refine and iterate.

Let's take a look at myPy in python for example:

def add_2(num):
    return num + 2
print(add_2(1))

This program just add 2 to every number we pass in. The num argument has no assertion, no requirement from the world. It is just requiring that the add_2 function will be called with 1 argument.

Using mypy, we can add a type hint:

def add_2(num:int): // <- Added type hint
    return num + 2
print(add_2(1))

Which increased the requirements of the code, not for execution but for testing (remember that now type checks are optional so we treat them as part of our test phase). Running mypy command line tool, will check that every call to add_2 passes a single argument which can be proved is an int.

Lets look at another example:

def add_2(num:int):
    return num + 2
print(add_2("magic")) // Note here we pass a string

We can still run the above python code even though it does not pass mypy type check. At runtime, when executed, the code will fail at runtime saying that the "operator + is not supported for str and int".

Summary

For a first post this is becoming too long so I will wrap it up here.

The key takeaway from this is that types can be very easy to introduce and improve our code quality.

Incrementally we can add types to our code, which like unit tests increase the amount of claims or assertions we have on our system. Unlike unit-tests, types are far easier to maintain, they have little overhead and even contribute to in-context code readability.

I hope you find this useful and it made you think about types a bit differently.

In the future I wish to explore what other checks we can add to the development flow that combine the easiness and robustness of asserting with a type system.