In March 2017, Henning Dieterichs opened issue 14833 against the TypeScript repository with a title and a disclaimer. The title: “TypeScripts Type System is Turing Complete”. The disclaimer: “This is not really a bug report and I certainly don’t want TypeScripts type system being restricted due to this issue. However, I noticed that the type system in its current form (version 2.2) is turing complete.”

Attached was a primality test written entirely in type declarations, with Peano numerals encoded as nested objects and a forceTrue helper that produces a type error when the answer is wrong. The team labeled the issue Discussion and left it open.

The idea

Turing completeness is not a fun fact about TypeScript, it is the constraint that shapes the whole feature area. Once the type level can express arbitrary computation, no compiler can decide in general whether a given type will finish resolving, so the checker cannot be correct and complete about it. What TypeScript ships instead is a budget: recursion depth caps, instantiation counters, and heuristics that fail loudly rather than hang. Every release note about type-level recursion is a negotiation over the size and shape of that budget, and every “excessively deep” error you have ever seen is the budget doing its job on a program that might well have terminated.

What made it complete

The construction predates conditional types entirely, which is the detail most people get wrong. In 2017 there was no T extends U ? X : Y. The device Dieterichs used is a dispatch table:

type MyFunc<TArg> = {
  "true": TrueExpr<MyFunc, TArg>,
  "false": FalseExpr<MyFunc, TArg>
}[Test<MyFunc, TArg>];

An object type with two branches, indexed by a type-level boolean encoded as a string literal. Selection by indexed access, not by a conditional. The issue lists the ingredients: “Turing completeness is being achieved by combining mapped types, recursive type definitions, accessing member types through index types and the fact that one can create types of arbitrary size.”

Four features, none of which was designed for computation. Mapped types were for Partial and Readonly. Indexed access was for reading a property type. Recursion in type aliases was for linked structures. Together they were accidentally a programming language, which is a recurring result: C++ templates and the C preprocessor got there the same way, by accumulating enough mechanisms that a fixed point appeared between them.

The immediate corollary is stated in the issue as well: “Besides (and a necessary consequence of being turing complete), it is possible to create an endless recursion”. The author even proposes the cure and names its cost: “Turing completeness could be disabled, if it is checked that a type cannot use itself in its definition”. Ban self-reference and the problem goes away, along with most of what the type level is used for. TypeScript did not take that trade.

Why there is a budget and not a proof

If the type level is Turing complete, deciding whether an arbitrary type resolves is the halting problem in a new costume. A compiler facing an undecidable question has three options: refuse the expressive power, run forever on some inputs, or approximate. TypeScript approximates, and says so in the language of engineering rather than theory.

The 4.5 release notes: “TypeScript often needs to gracefully fail when it detects possibly infinite recursion, or any type expansions that can take a long time and affect your editor experience. As a result, TypeScript has heuristics to make sure it doesn’t go off the rails when trying to pick apart an infinitely-deep type, or working with types that generate a lot of intermediate results.”

The word doing the work there is heuristics. The 4.1 notes describe the same wall from the user’s side: “these types can hit an internal recursion depth limit on sufficiently-complex inputs. When that recursion limit is hit, that results in a compile-time error.” The message is Type instantiation is excessively deep and possibly infinite, and the possibly is honest. The compiler is not claiming your type diverges. It is claiming it ran out of budget before finding out.

That gap between “would terminate” and “terminates within budget” is where the practical pain lives. A recursive string type that a human can prove finite by induction on the length of the input is rejected anyway, because the checker counts instantiations rather than constructing the proof. The 4.5 notes give the case exactly: a leading-space trim “can be useful, but if a string has 50 leading spaces, you’ll get an error.”

Widening the budget

TypeScript 4.1 first made deep recursion legal at all: “In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.” That is what unlocked Awaited and the deep-flatten types the standard library now ships.

TypeScript 4.5 then made one shape of recursion much cheaper. “But there’s a saving grace: TrimLeft is written in a way that is tail-recursive in one branch. When it calls itself again, it immediately returns the result and doesn’t do anything with it. Because these types don’t need to create any intermediate results, they can be implemented more quickly and in a way that avoids triggering many of type recursion heuristics that are built into TypeScript.”

The rule is syntactic: “As long as one branch of a conditional type is simply another conditional type, TypeScript can avoid intermediate instantiations. There are still heuristics to ensure that these types don’t go off the rails, but they are much more generous.”

And the workaround for types that do not qualify is the one a functional programmer would reach for. A character-collecting type that unions its result with the recursive call is not tail-recursive, so “If you would like to make it tail-recursive, you can introduce a helper that takes an ‘accumulator’ type parameter, just like with tail-recursive functions.” The accumulator transform is the same rewrite, applied to a language whose call stack is the compiler’s instantiation stack.

The cost lands on the people reading your code

The 4.1 notes attach unusually blunt advice: “Keep in mind that while these recursive types are powerful, they should be used responsibly and sparingly. First off, these types can do a lot of work which means that they can increase type-checking time. Trying to model numbers in the Collatz conjecture or Fibonacci sequence might be fun, but don’t ship that in .d.ts files on npm.” The closing line is the one worth pinning up: “In general, it’s better not to use these types at all than to write something that fails on more realistic examples.”

A clever type does not cost you once at authoring time. It costs every consumer on every build and every keystroke in their editor, and it fails on inputs slightly larger than the ones you tested. That asymmetry, between who writes the type and who pays for it, is the actual argument against type-level cleverness, and it holds even for types that never hit the depth limit.

Where this leaves the folder

Every feature in this section is a step toward the same destination. Conditionals gave branching, infer gave binding, mapped types gave iteration, and template literals gave strings to compute over. The result is a real language with real programs in it, and the same result it always is: expressive power arrives with an undecidable termination question, and the implementation answers it with a counter.

The interesting question about a TypeScript type is no longer whether it can be written. It is whether it should be, and what it will cost the next person to compile.

Sources

  • Henning Dieterichs, “TypeScripts Type System is Turing Complete,” microsoft/TypeScript issue 14833. https://github.com/microsoft/TypeScript/issues/14833 . Supports the March 2017 date and authorship, the observation about version 2.2, the four features that combine to give Turing completeness, the indexed dispatch device, the endless recursion corollary, and the proposed self-reference restriction.
  • TypeScript 4.1 release notes, “Recursive Conditional Types.” https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html . Supports conditional types referencing themselves within their branches, the increase in type-checking time, the advice against shipping Collatz and Fibonacci types on npm, the internal recursion depth limit and its compile-time error, and the closing recommendation.
  • TypeScript 4.5 release notes, “Tail-Recursion Elimination on Conditional Types.” https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html . Supports the graceful-failure heuristics, the excessively deep error, the fifty-leading-spaces failure, the tail-recursive branch condition and its more generous limits, and the accumulator workaround.