Chapters

  • 0. Introduction

    Free

  • 1. Types & Values

    Free

  • 2. Types are just data

    Free

  • 3. Objects & Records

    Free

  • 4. Arrays & Tuples

    Free

  • 5. Conditional Types
  • 6. Loops with Recursive Types
  • 7. Template Literal Types
  • 8. The Union Type Multiverse
  • 9. Loops with Mapped Types
  • 10. Assignability In Depth

    wip

  • 11. Debugging Types

    wip

Articles

Subscribe

About

Made with ❤️ by @GabrielVergnaud

Types & values

First, let's make an important distinction between the language of values and the language of types. The language of values lets us write the code that will run in production and will do helpful stuff for our users. The language of types, however, is completely erased before the code reaches our users. It's only there to help TypeScript make sure the code doesn't contain mistakes before we ship it.

JavaScript doesn't have types, so naturally all of JavaScript is value-level1 code:

// A simple Javascript function:
function sum(a, b) {
  return a + b;
}

TypeScript lets us add type annotations to JavaScript and make sure the sum function we wrote will never be called with anything other than numbers:

// Using type annotations:
function sum(a: number, b: number): number {
  return a + b;
}

But the type system of TypeScript is much more powerful than that. The real-world code we write sometimes needs to be generic and to accept types we don't know in advance.

In that case, we can define type parameters in angle brackets <A, B, ...> and assign them to value parameters with a: A. We can then pass type parameters to a type-level function which computes the output type from the types of inputs:

// Using type level programming:
function genericFunction<A, B>(a: A, b: B): DoSomething<A, B> {
  return doSomething(a, b);
}

This is what type-level programming is! DoSomething<A, B> is a type-level function written in a peculiar programming language that is different from the language we use for values, but just as powerful. Let's call this language Type-Level TypeScript.

// This is a type-level function:
type DoSomething<A, B> = ...

// This is a value-level function:
const doSomething = (a, b) => ...

The language of types

Type-level TypeScript is a minimal, purely-functional language.

The term "functional" in that definition refers to Functional Programming, a concept you might have heard of before. Type-level TypeScript is functional simply because functions are the main means of abstraction in this language. We will use functions all the time.

At the type level, functions are called generic types: they take one or several type parameters and return a single output type. Here is a simple example of a function taking two type parameters and wrapping them in a tuple:

type SomeFunction<A, B> = [A, B];
/*                ----    ------
                   ^         \
                  type        return type
               parameters

     \-------------------------/
                 ^
              Generic
*/

Type-level TypeScript doesn't have a lot of features. After all it was designed exclusively to type your code! That said, it does have enough features to be (almost) Turing Complete, which means you can solve problems of arbitrary complexity with it.

Here are some of the things you can do with Type-Level TypeScript:

  • Code branching: executing different code paths depending on a condition (the equivalent of the value-level if/else keywords).
  • Variable assignment: declaring a variable and using it in an expression (the equivalent of the value-level var/let keywords).
  • Functions: re-usable bits of logic like the one we have seen in the previous example.
  • Loops: usually through recursion.
  • Equality checks: == but for types!
  • And much more!

And here are some things you can not do:

  • No Mutable state: You can't re-assign a variable to a new value at the type level.
  • No Input/Output: You can't perform side effects such as logging something to the console, reading a file or making an HTTP request at the type level. That's fortunate: I really would not want my type system to read my files and send them to some server!
  • No Higher-Order Functions: You can't pass a function to another function in type-level TypeScript. This is a very common pattern at the value level. For example .map, .filter and .reduce are higher-order functions. This means we won't be able to implement these at the type level. In practice, this limitation isn't so bad because type-level algorithms are usually simpler.

That was a brief overview of the kind of language we will be learning in the upcoming chapters. Now, let's jump to our first challenge!

How challenges work

At the end of each chapter, you will have a few challenges to solve to put your new skills into practice. They look like this:

namespace challenge {
  // 1. implement a generic to get the union
  // of all keys in an object type.
  type GetAllKeys<Obj> = TODO;

  type res1 = GetAllKeys<{ a: number }>;
  type test1 = Expect<Equal<res1, "a">>;
}
  • namespaces are a lesser-known TypeScript feature which lets us isolate each challenge in a dedicated scope.
  • TODO is a placeholder. This is what you need to replace!
  • type res1 = ... is the type returned by your generic for some input type. You can hover it with your mouse to check its current value.
  • type test1 = Expect<Equal<res1, ...>> is a type-level unit test. It won't type-check until you find the correct solution.

I sometimes use @ts-expect-error comments when I want to check that invalid inputs are rejected by the type-checker. @ts-expect-error only type-checks if the next line does not!

// @ts-expect-error ✅ this type-checks because
let x: number = "Hello"; // this line does not.

// @ts-expect-error ❌ this doesn't type-check because
let y: number = 2; // this line does!

Challenges

Ready to solve your first challenges? Let's go!

Challenge
Solution
Challenge
Solution
Challenge
Solution
Challenge
Solution

Congratulations! 🎉

Footnotes

  1. In computer science lingo, people often talk about terms rather than values to distinguish regular code from types. I find the term "term" a little more confusing than "value" and I haven't heard it much in my career, that's why I decided to stick with talking about value-level and type-level code. I believe that values are a concept that feels familiar to more developers. ↩

If you enjoyed this free chapter of Type-Level TypeScript, consider supporting it by becoming a member!

Support Type-Level TypeScript!

Become an Early-Bird Member to support the writing of Type-Level TypeScript and get access to all upcoming chapters!

You will find everything you need to become a TypeScript Expert — 11 chapters of in-depth, unique content, and more than 70 fun challenges to practice your new skills.

  • Full access to all 11 chapters

  • 70 type challenges with explanations

  • Lifetime access to all course materials

  • Exclusive discord community

Loading...

⟸ Previous

0. Introduction

Next ⟹

2. Types are just data