Types are a complex language of their own

I used to think of TypeScript as just JavaScript with type annotations sprinkled on top of it. With that mindset, I often found writing correct types tricky and daunting, to a point they got in the way of building the actual applications I wanted to build, and frequently, it led me to reach for any. And with any, I lose all type safety.

Indeed, types can get really complicated if you let them. After writing TypeScript for a while, it occurred to me that the TypeScript language actually consists of two sub-languages - one is JavaScript, and the other is the type language. For the JavaScript language, the world is made of JavaScript values; for the type language, the world is made of types. When we write TypeScript code, we are constantly dancing between these two worlds: we create types in our type world and "summon" them in our JavaScript world using type annotations; we can go in the other direction too: use the typeof operator on JavaScript variables/properties to retrieve the corresponding types.

The JavaScript language is very expressive, so is the type language - in fact, the type language is so expressive that it has been proven to be Turing complete.

Here I don't make any value judgment of whether being Turing complete is good or bad, nor do I know if it is even by design or by accident (in fact, many times, Turing-completeness was achieved by accident). My point is the type language itself, as innocuous as it seems, is certainly powerful, highly capable and can perform arbitrary computation at compile time.

When I started to think of the type language in TypeScript as a full-fledged programming language, I realized it even has a few characteristics of a functional programming language:

  1. use recursion instead of iteration
  2. types (data) are immutable

In this post, we will learn the type language in TypeScript by comparing it with JavaScript so that you can leverage your existing JavaScript knowledge to master TypeScript quicker.

This post assumes that readers have some familiarity with JavaScript and TypeScript. And if you want to learn TypeScript from scratch properly, you should start with The TypeScript Handbook. I am not here to compete with the docs.

Variable declaration

In JavaScript, the world is made of JavaScript values, and we declare variables to refer to values using keywords var, const and let. For example:

const obj = {name: 'foo'}

In the type language, the world is made of types, and we declare type variables using keywords type and interface. For example:

type Obj = {name: string}

The idea of "type variables" is a made-up concept - a type variable is an alias of a type, analogous to how a JavaScript variable references a value. I found drawing this analogy makes explaining concepts of the type language much easier.

Types and values are very related. A type, at its core, represents the set of possible values. Sometimes the set is finite, e.g., type Name = 'foo' | 'bar', a lot of times the set is infinite, e.g., type Age = number. In TypeScript we integrate types and values and make them work together to ensure that the runtime values match the compile-time types.

We talked about how you can create type variables in the type language. However, the type variables have a global scope by default. To create a local type variable, we can use the infer keyword in our type language.

type A = 'foo'; // global scope
type B = A extends infer C ? (
    C extends 'foo' ? true : false// *only* inside this expression, C represents A
) : never