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

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.

Types as sets

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 and never
    • Can be read as meaning that a value is truly unknown, so it could be anything
  • Never
    • Is equivalent to the empty set (sometimes called the null set)
    • Means that it’s a type that never has a value
  • 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.

A chess board showing the concept of n queens

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 another thing, or something else, …
    • The union type alias StringOrNumber of string and number is type StringOrNumber = string | number
    • It can be read as “StringOrNumber is either a string or a number
    • Discriminating unions offer pattern matching capabilities to programmatically winnow a union of types into a specific type
  • Intersections $A \cap B$ say that a type is both a type A, and a type B, and C, …
    • The intersection type alias TwoObjects of { a: number } and { b: number } is equivalent to type TwoObjects = { 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

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 the type
  • teal is the type 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.