Introduction to Type Theory for Programmers

12 minute read Published: 2025-03-22

A program can be thought of as functions. They take some input data and produce some output data. Each value has a type associated with it. 32.1 is a number and "hello!" is a string. Types for the same conceptual value might not be the same in every programming language. In C, C++, Java, and Python, 32.1 is a float. Unlike JavaScript, these languages differentiate between integers and other real numbers.

JavaScript in 111 Seconds

To get on the same page, this post is going to use JavaScript for its examples. If you already know JavaScript and TypeScript, you can skip ahead.

Here are some different types in a few programming languages:

JavaScriptC/C++JavaPython
Real Number ($\mathbb{R}$)numberint

unsigned int

float

double

int

float

double

int

float

Stringstringchar *Stringstr
Nothingundefined

null

voidvoidNone

This is not an exhaustive list but it should be enough to get us up and running.

To get familiar with the syntax of JavaScript, I'm going to write the same functions in each of these languages:

JavaScript

function average(a, b) {
   return (a + b) / 2;
}

function exclaim(s) {
   return s + "!";
}

TypeScript

function average(a: number, b: number): number {
   return (a + b) / 2;
}

function exclaim(s: string): string {
   return s + "!";
}

C / C++

float average(float a, float b) {
   return (a + b) / 2;
}

char *exclaim(char const *s) {
   size_t old_len = strlen(s);
   size_t new_len = old_len + 1;
   char *out = malloc(new_len + 1); // +1 for '\0'
   memcpy(out, s, old_len);
   out[old_len] = '!';
   out[new_len] = '\0';
   return out;
}

Java

float average(float a, float b) {
   return (a + b) / 2;
}

String exclaim(String s) {
   return s + "!";
}

Python

def average(a, b):
   return (a + b) / 2

def exclaim(s: str) -> str:
   return s + "!"

Besides the C one that took some memory finagling, they all more or less look the same. There are two things of note:

  1. The TypeScript is almost identical to the the JavaScript. The only difference is types attached using : type.
  2. The Python program has types listed for exclaim but not average. That's because average(a: int, b: int) -> float wouldn't quite be accurate. a and b can be either int or float1.

The rest of this post will use JavaScript (and eventually TypeScript). We won't use advanced syntax in this post.

Dynamic Typing

In dynamically typed languages like JavaScript and Python, you don't always know the type of a variable when you're writing the code.

Let's look at an example program in JavaScript:

function average(a, b) {
   return (a + b) / 2;
}

I can call this function in several different ways:

average(4, 2);         => 3
average(5.5, 4.5);     => 5
average(4, "2");       => 21
average("uh", "oh");   => NaN

Depending on the types of the arguments, something very different happens. Particularly on lines 3 and 4. In JavaScript, when you add two numbers, you get another number. But when you add two strings, you get another string. Additionally, anything plus a string gets you another string. People call this behavior weak-typing2.

 1   +  2  == 3
"1"  + "2" == "12"
 1   + "2" == "12"
null + "y" == "nully"

Graduating to Static Typing

We can do better. Let's switch to TypeScript and start adding types to our arguments. We'd like to prohibit inputs that make the function behave strangely.

1function average(a: number, b: number): number {
2 return (a + b) / 2;
3}
4
5average(4, "2");
6average("uh", "oh");

Argument of type 'string' is not assignable to parameter of type 'number'.

As desired. In this case, we used types to constrain the function domain, thereby shrinking the program execution space to something more representative of "average". A smaller execution space is easier to reason about.

With static typing, we can catch rare bugs:

1let x;
2if (Math.random() > 0.001) {
3 x = average(9, 9);
4} else {
5 // very rare!
6 // compiler catches the bug anyways
7 x = average("9", 9);
8}

Easing Into Math

Our next code sample is rather contrived:

1function times_two(x: number): number {
2 const y = Math.floor(x) * 2;
3 if (y % 2 === 0) {
4 return y;
5 }
6
7 //return
8}

Function lacks ending return statement and return type does not include 'undefined'

We, as humans, know that y is always an integer multiple of 2. y modulo 2 must be 0. Unfortunately, TypeScript isn't that clever yet.

Let's try and talk about that in a more mathematical way. The primary motivation for math in this case is terseness, formality, and precision. To do that, we'll need to briefly cover logic and set theory. If you know these, you can skip ahead.

Sets are pretty great. They're similar to arrays or lists in that they contain elements, but a set cannot have duplicate elements. A value is either in a set or it isn't. Unlike a list, the order of elements within a set does not matter.

A set is written using braces. The following are sets containing only 1, 2, and 3 and they are all equivalent:

To write "a is an element of A", we write $a\,\in\,A$.

$$ \begin{array}{rcllr} 1&\in &\{1, 2, 3\}& \quad& \text{True}\\ 4&\in &\{1, 2, 3\}& &\text{False}\\ 4&\cancel{\in} &\{1, 2, 3\}&& \text{True}\\ \end{array} $$

You can also construct sets using other sets. The syntax is $\{\textit{value}\,|\,\textit{conditional}\}$. For example:

$$ \begin{align} \text{let}\,A &= \{1, 2, 3\} \\ B &= \{2 * a\,|\,a\in A\} \\ B &= \{2, 4, 6\} \end{align} $$

There are also some common set names:

TypeScriptMathEnglish
function times_two(x: number)
$$ x \in \mathbb{R} $$

x is some real number.

   const y = Math.floor(x) * 2;
$$ y = 2 * \text{floor}(x) $$

y is 2 times the floor of x.

$$ y \in \{2a\,|\,a \in \mathbb{Z}\} $$

y is an integer multiple of 2.

   if (y % 2 === 0) {
      return y;
   }

$$ \therefore mod(y, 2) = 0 $$

Therefore, y modulo 2 is 0. The conditional is always true and y is returned.

Was this argument convincing? I think it was. But there's a huge problem: we had to use our brain. Brains can make mistakes and can be convinced of things that are untrue so this proof is ungood.

Assembling a Type Theory

It'd be just great if we could mechanize this process. Once again, I will introduce more notation3:

$$ \frac{\textit{Premise}_1\quad...\quad\textit{Premise}_n}{\textit{Conclusion}}\textit{Name} $$

This is no fraction! This is an inference rule. Let's look at a few examples:

$$ \frac{\text{Raining}}{\text{Wet In My Garden}} $$

$$ \frac{\text{Sprinklers On}}{\text{Wet In My Garden}} $$

$$ \frac{\text{Flowers in My Garden} \quad \text{Wet In My Garden}}{\text{Flowers Watered}} $$

From these rules, I can prove that if it is raining and I have flowers in my garden, then my flowers are watered.

$$ \frac{\dfrac{\text{Raining}}{\text{Wet In My Garden}}\quad\text{Flowers in My Garden}}{\text{Flowers Watered}} $$

Also, if we can prove something without a premise, we simply omit the premises entirely:

$$ \frac{}{\text{Types Are Wonderful!}} $$

Fun, right? Let's get some type theory going by writing inference rules for the JavaScript + operator.

First, recursively define JavaScript expressions and create notation:

Subscripting any of these just distinguishes them. $n_1$ and $n_2$ are both Reals, but not necessarily the same one.

Here are some JavaScript expressions written out in symbols:

$$ [s] $$

$$ [e_1 + s] $$

$$ [n + e_1 + s] $$

$$ [e_2 + e_1 + n + n] $$

For clarity, let's always use brackets around the whole expression.

Next, let's define our types.

Now, I'm not going to tell you what $\diamond$ and $\omega$ are but I will show you how they're used.

$$ \frac{}{[n] : \diamond} $$

$$ \frac{}{[s] : \omega} $$

$$ \frac{[e_1] : \diamond \quad [e_2] : \diamond}{[e_1 + e_2] : \diamond} $$

$$ \frac{[e_1] : \omega \quad [e_2] : \diamond}{[e_1 + e_2] : \omega} $$

$$ \frac{[e_1] : \diamond \quad [e_2] : \omega}{[e_1 + e_2] : \omega} $$

$$ \frac{[e_1] : \omega \quad [e_2] : \omega}{[e_1 + e_2] : \omega} $$

Recall the rules of the JavaScript + operator:

number + number => number
string + any    => string
any    + string => string

Try to understand the notation and guess what $\diamond$ and $\omega$ mean. Then open the answer below.

Answer

When I write $[e] : \tau$, read "the JavaScript expression $e$ has type $\tau$".

  • $\diamond$ is for numbers
  • $\omega$ is for strings

These are not sets. They are simply symbols that we'll attach to JavaScript expressions using the colon.

Type TheoryEnglish
$$ \frac{e_1 : \diamond \quad e_2 : \omega}{[e_1 + e_2] : \omega} $$

If the type of $e_1$ is number and the type of $e_2$ is string,

then the type of $e_1 + e_2$ is string.

Functions, Variables, and Context! Oh My!

What about functions and variables? For those, we will have to extend both the JavaScript expression grammar and the type grammar:

"Wait, where are the functions I was promised?", you ask.

Instead of functions, we're going to have lambdas4. They work like so: $\textit{Parameter Name} \Rightarrow \textit{Return Expression}$

function add_three(a) {
   return a + 3;
}

// equivalent

add_three = a => a + 3;

Now consider the following program:

$$ [(x \Rightarrow x + 1)(2) + (x \Rightarrow x + 1)(\textcolor{green}{\texttt{"2"}})] $$

What is the type of $[x + 1]$? Well, that depends on the context, doesn't it?

$$ \frac{[x] : \diamond}{[x + 1] : \diamond} $$

$$ \frac{[x] : \omega}{[x + 1] : \omega} $$

But this notation only allows us to have one type for $x$. We'll have to think differently now.

Introducing $\Gamma$ (Gamma), the type context! Think of it like a dictionary of resolved types for variables5.

Γ = {v1: 𝜏1, v2: 𝜏2, ...};

When I write $\Gamma \vdash [x] : \tau$, read "In a certain context $\Gamma$, $x$ has type $\tau$"6.

Using this new notation, I can stuff type judgements like "$x$ has type $\tau$" into $\Gamma$.

$$ \frac{}{\Gamma \vdash [x] : \tau} $$

And I can "query" $\Gamma$ for "$y$ has type $\diamond$" and conditionally do something if it exists.

$$ \frac{\Gamma \vdash [y]: \diamond}{\text{Something}} $$

Let's update our inference rules.

$$ \frac{}{[n] : \diamond} $$

$$ \frac{}{[s] : \omega} $$

$$ \frac{\Gamma \vdash [e_1] : \diamond \qquad\Gamma \vdash [e_2] : \diamond}{\Gamma \vdash [e_1 + e_2] : \diamond} $$

$$ \frac{\Gamma \vdash [e_1] : \tau \qquad\Gamma \vdash [e_2] : \omega}{\Gamma \vdash [e_1 + e_2] : \omega} $$

$$ \frac{\Gamma \vdash [e_1] : \omega \qquad\Gamma \vdash [e_2] : \tau}{\Gamma \vdash [e_1 + e_2] : \omega} $$

‼️ $$ \frac{\Gamma \\,[v] : \tau_1 \vdash [e] : \tau_2}{\Gamma \vdash [v \Rightarrow e] : \tau_1 \rightarrow \tau_2} $$
‼️ $$ \frac{\Gamma \vdash [e_1] : \tau_1 \rightarrow \tau_2 \qquad \Gamma \vdash [e_2] : \tau_1}{\Gamma \vdash [e_1(e_2)] : \tau_2} $$

There are two brand new rules. I would encourage you to try and work them out on your own and then check your understanding below.

The Last Two Rules
Type TheoryEnglish
$$ \frac{\Gamma \\,[\textcolor{teal}{v}] : \textcolor{blue}{\tau_1} \vdash [\textcolor{magenta}{e}] : \textcolor{purple}{\tau_2}}{\Gamma \vdash [\textcolor{teal}{v} \Rightarrow \textcolor{magenta}{e}] : \textcolor{blue}{\tau_1} \rightarrow \textcolor{purple}{\tau_2}} $$

This one makes more sense if we have an actual example. Let us instantiate $v$ and $e$:

$$ \textcolor{teal}{v} = x\qquad \textcolor{magenta}{e}=[\textcolor{teal}{x} + 1] $$

Hypothetically, if we add the typing judgement $\textcolor{teal}{x} : \textcolor{blue}{\diamond}$ to the context $\Gamma$, that causes $\textcolor{magenta}{x + 1}$ to have type $\textcolor{purple}{\diamond}$.

Because of that, a function whose parameter is $\textcolor{teal}{x}$ and returns $\textcolor{magenta}{x + 1}$ has the type $\textcolor{blue}{\diamond} \rightarrow \textcolor{purple}{\diamond}$

$$ \frac{\Gamma \vdash [e_1] : \tau_1 \rightarrow \tau_2 \qquad \Gamma \vdash [e_2] : \tau_1}{\Gamma \vdash [e_1(e_2)] : \tau_2} $$

If you have a function $e_1$ that takes $\tau_1$ and returns $\tau_2$, then calling it with an argument $e_2$ of type $\tau_1$ gets you $\tau_2$.

Thank you for reading and happy typing!

I;m Thinking About Thos Types :)

Citations

  1. F. Shinko, Introduction to type theory. [Online]. Available: https://math.berkeley.edu/~forte/notes/type_theory.pdf. Accessed: Mar. 12, 2025.
  2. H. Geuvers, Introduction to Type Theory. [Online]. Available: https://www.cs.ru.nl/~herman/onderwijs/provingwithCA/paper-lncs.pdf. Accessed: Mar. 13, 2025.
  3. B. C. Pierce, Types and Programming Languages Cambridge, MA, USA: MIT Press, 2002.

Footnotes

1

int | float is a valid way to write that, but I didn't want to introduce union types just yet.

2

People call things weakly-typed when they don't like what happens.

3

This notation is called "Gentzen-style Natural Deduction"

4

Praise Church. Search up "Lambda Calculus" if you want to learn more.

6

$\Gamma \vdash ...$ is actually the same thing as $\frac{\Gamma}{...}$, but that would be too confusing to explain inline.

5

This is a half-truth, only useful insofar as it helps intuition.