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:
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 to make sure invalid inputs are rejected by the type-checker. This line will only type check if the next line does not!
Challenges
Ready to solve your first challenges? Let's go!
Congratulations! 🎉
Footnotes
-
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!
Enroll in Type-Level TypeScript!
Get access to all chapters of the Type-Level TypeScript course and join a community of 1600+ students!
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...