TypeScript's static (compile-time only) type system is very powerful. Its modern features such as type inference, conditional types, mapped types, and generics enable developers to write safe and expressive code at the cost of spending more time wrangling types. All while catching errors at compile-time and avoiding runtime type-checking overhead for internal code (we still need runtime type-checks for external input, of course - I like Zod!).
It just so happens that in tandem, these features in TypeScript's type system reach critical mass: Turing completeness. This is just a fancy way of saying that we can design arbitrary programs in this "language" that can solve any complex problem given enough time and compute.
You read that right! TypeScript's built in type utilities satisfy the basic requirements for Turing completeness without any runtime JavaScript code:
type IsString<T> = T extends string ? true : false;
type NestedRecord<T> = { [key: string]: T | NestedRecord<T>}
type StringToArray<S extends string> =
S extends `${infer First}${infer Rest}`
? [First, ...StringToArray<Rest>]
: [];
This comes with a big caveat: we can't actually run these programs in the traditional sense, since there is no runtime code! In order to view the output of our programs, we'll need to rely on the TypeScript compiler which does the heavy lifting of parsing and computing type values (as well as emitting JavaScript code, but we don't care about that here!).
An easy way to test our programs is to
use the TypeScript Playground and hover over the type we designated as the output of our program
to see the value our program computed. Try it out! Here's the StringToArray program from above, in the playground - just hover over hover_me
to see the value our program computed. This process is simply the TypeScript Compiler
communicating the type values to the editor via a Language Server, and the editor
displaying the type values to the user on hover.
We can do some cool, esoteric things with the type system. Things as simple as using types to compute Fibonacci numbers, and as complex as building a SQL database, a state machine, or a full fledged parser for a meta language. What does this mean for most developers? Not much, for the most part.
However, for library developers, it's a completely different story (note: not that different lol, TS could be NOT Turing complete and all of this would still be possible). Library developers commonly use type-level programming to encapsulate complex library details into simple, strongly-typed APIs. This is the true value of the depth of TypeScript's type system: library developers can deliver amazing developer experiences that enable users to write end-to-end type-safe code.
Example: Zod. It has such a simple user-facing API, yet it requires complex type-level programming behind the scenes to extract TypeScript types from schemas, handle deep nesting, and parse custom types. Other libraries like tRPC and Prisma are similar - type-safety for users, type-complexity for library developers.