- For a more complete guide check out Type Level Typescript by Gabriel Vergnaud
TypeScript offers a powerful way of introducing constraints in a codebase. These constraints produce self-documenting, bug free code. Developer experience benefits immensely from the effective use of types. But TypeScript can be difficult, as types can be arcane and errors verbose.
Developers looking to learn TypeScript often encounter a world of complicated terminology and unintuitive explanations: algebraic data types, homomorphism, and so on. This formal language may be correct, but is only useful to those familiar with it.
Set theory comes naturally to many programmers. Most languages use sets of some form. Python uses them extensively. JavaScript has them too. In TypeScript, types can be thought of as sets.
Set Theory and TypeScript
The word with the most meanings in English is the word ‘set’. The Oxford English Dictionary lists 430 possible definitions, and the entry is over 60,000 words long.
— Quite Interesting (@qikipedia) March 5, 2020
One way to grok sets is to consider how they are used in everyday language. A set is just a collection or group of things. Just as we categorise things in daily life, so too do we categorise things when we use types. In TypeScript, the string
type can be thought of as the set of all values that are strings.
The most important thing to note here is that this is a constraint on possible values. The convention that types are categories (sets) of things means that they have a separate existence to other things. A string
is a category of thing, and a number
is a category of another type of thing. These categories are established by underlying types in JavaScript.
To assign something to the type string
is to constrain that thing to be only of the type string
. This principle underlies the value proposition of TypeScript.
Unknown, Never, and Any
There are three high level types worth knowing well. These are unknown
, never
, and any
.
- Unknown
- Is similar to the
universal set
- Is the superset of all types, other than
any
andnever
- Can be read as meaning that a value is truly unknown, so it could be anything
- Is similar to the
- Never
- Is equivalent to the
empty set
(sometimes called thenull set
) - Means that it’s a type that never has a value
- Is equivalent to the
- Any
- Disables type checking entirely
- Is akin to using JavaScript, not TypeScript
- Is not really a type, it just disable checks
Broadly speaking, we talk about types being supersets and subsets. Supersets in the most general sense are categories that contain other categories. Subsets, the inverse.
The string
type is therefore the superset of all string values. But not string literals
. Types with a domain of values that include those of a second type can be assigned to that second type.
Larger types with a domain of values that include the domain of smaller types are subsets of the smaller type. And vice versa, the smaller type is a superset of the larger type. So a string literal is also a string, but a string is not a string literal.
We create categories of things all the time. Boxes can be categories of things into which we put items of a specific nature. We might call that box a Box
(type), and constrain it to only hold crayons
and paper
(values).
Thinking about types as sets/categories of things helps establish their purpose. They define logical relations that limit values to being of a certain type.
Constraint Satisfaction
Constraint satisfaction problems (CSPs) are common in artificial intelligence and computer science. They involve problems in which the optimal solution (or just a good solution) is best sought by introducing constraints that make the problem simpler. A famous example is the N-queens problem in which a queen is placed on every column of a chessboard, such that no queen is attacking another.
If we were to place queens randomly, our probability of success would be very low. But by accounting for queens that have already been placed on the board, we can limit the state space to a subset of that space that does not violate the rule.
In a similar vein, types in TypeScript are most effective when thought of as constraints. They limit what a codebase can do, and this is a good thing. They empower developers to reduce complexity, as they allow us to limit the possible range of values that things can take on.
In the TypeScript Handbook, this general idea is called narrowing.
Unions and Intersections
When we perform operations on types in TypeScript it is useful to keep in mind the so-called universal set
. This is a catch-all category for all possible things. In this context, it means the set of all possible values.
It’s the values that ultimately matter. Types merely constrain what those values can be.
Unions and intersections in TypeScript are used in a different way than in set theory. But they are analogous.
- Unions $A \cup B$ say that a type is either one
thing
, or anotherthing
, or somethingelse
, …- The union type alias
StringOrNumber
ofstring
andnumber
is typeStringOrNumber
= string
|
number
- It can be read as “
StringOrNumber
is either astring
or anumber
” - Discriminating unions offer pattern matching capabilities to programmatically winnow a union of types into a specific type
- The union type alias
- Intersections $A \cap B$ say that a type is both a type
A
, and a typeB
, andC
, …- The intersection type alias
TwoObjects
of{
a: number
}
and{
b: number
}
is equivalent to typeTwoObjects
=
{
a: number, b: number
}
- When applied to an object, the intersection acts like what we might expect of a union. One way to think about this is that both sets in the example above are subsets of a superset. That superset is
{
a: number, b: number
}
. Both sets have this property in common, and so this is the intersection
- The intersection type alias
Both unions and intersections are themselves categories of type, just as the primitive string
is, or string literal "dog"
is.
Assignability and Constraints
Looking at types as constraints implies that they impose limits on what values with a type can be. Variables can be declared and assigned a type without a value. We can provide a value later, and that value must accord with the constraints of the type.
Assignment can be thought of as always occuring in relation to the universal set
. This contains all possible values. Variables in JavaScript do not have constraints as they can be assigned to any value. Once a value has been provided, we might determine it with typeof
, or the infamous Array.
isArray
. But this doesn’t speak towards placing a limit on the variable itself, which could be anything.
In this lingo, that variable is the universal set
. Given that we have some intention and idea of how it is to be used, we can constrain the variable such that it can only be used in certain ways.
TypeScript automates this process and enforces constraints across a codebase.
The type system of TypeScript is based on how JavaScript is used. Equivalence is based on membership, so if two types project the same codomain, then they are considered to be the same. Functions present a trickier case where parameters must be accounted for.
Generics and Naming Conventions
These are a common point of confusion. A generic is just the name of a type variable. But this is different from a type alias.
Notice that:
blue
is thetype
teal
is thetype alias
A type alias is always a type of something. The alias name points to an underlying type. These underlying types are somewhat implied, which is why larger sets of objects are subsets of smaller sets that contain the same key and property types. It’s also why Hello
and World
are equivalent in the example above. They both point to the same underlying type, and the thing that points is the type alias.
Generics are part of a separate thing called type variables. These address the problem where we do not know in advance what a type should be. This is easiest to explain with a concrete example:
Conditional Types and Infer
Conditional types are implemented with the extends
keyword. This lets us express a degree of logic to determine what a type should be at the time that it is used.
Within a conditional expression we can use the infer
keyword. This can unwrap values and differentiate the type of a variable by accounting for its value.
There are two additional forms of conditional type described by TypeScript:
- Distributive conditional types apply a conditional type expression to a union of types
- Deferred conditional types determine types at the time that they are used
Infer
is always used inside a conditional type expression.
Advanced Types
Index Signatures
Index signatures constrain property names of a given type to have a value of a given type. In the example below, all keys must be a string and all values must be a string.
Recursive Types
Recursive types model data structures that are recursive in nature. In practice TypeScript can have issues when the depth of a recursive type exceeds a certain level.
Mapped Types
Mapped types are similar syntactically to index signatures. They iterate over the keys of a type and return a new type with a new property type for those keys.
Reduce Complexity by Constraining Complexity
A key value proposition of TypeScript is the ability to place limits. It’s easier to reason about what code cannot do than what it can.