Metaprogramming in TypeScript: How to Write Code That Writes Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: The Magic Hidden in Types

\n\n

Imagine you are writing not just code, but a set of instructions for the compiler itself, which, based on your rules, generates safe, self-documenting constructs. This is metaprogramming in TypeScript — the ability to write code that works at the type level, analyzing and transforming other types during compilation.

\n\n

Many developers stop at basic generics and interfaces, unaware of the powerful arsenal hidden behind keywords like infer, extends, and conditional constructs. In this article, we will explore advanced metaprogramming techniques in TypeScript that will transform you from a simple user of the language into its true architect.

\n\n

1. Conditional Types: If at the Type Level

\n\n

Conditional types are the foundation of any type-level programming. They work like a ternary operator, but at the type system level:

\n\n
type IsString<T> = T extends string ? "yes" : "no";\n\ntype Test1 = IsString<"hello">;   // "yes"\ntype Test2 = IsString<42>;        // "no"\n
\n\n

But the real magic begins when we combine them with generics. For example, let's create a type that extracts only the string keys of an object:

\n\n
type StringKeys<T> = {\n  [K in keyof T]: T[K] extends string ? K : never\n}[keyof T];\n\ninterface User {\n  name: string;\n  age: number;\n  email: string;\n}\n\ntype UserStringKeys = StringKeys<User>; // "name" | "email"\n
\n\n

This example shows how advanced TypeScript types allow you to filter and transform data structures at compile time. You are literally programming logic that executes before the code reaches runtime.

\n\n

2. Infer: Extracting Types from Other Types

\n\n

The infer keyword is perhaps the most powerful tool in the TypeScript metaprogramming arsenal. It allows you to "look inside" a type and extract its constituent parts. Let's look at a classic example — getting the return type of a function:

\n\n
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;\n\ntype Fn = (x: number) => string;\ntype FnReturn = ReturnType<Fn>; // string\n
\n\n

But infer can do much more. Let's extract the type of the first argument of a function:

\n\n
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;\n\ntype Handler = (id: number, name: string) => void;\ntype HandlerFirstArg = FirstArg<Handler>; // number\n
\n\n

And what about recursive parsing of promises? Let's create a type that "unwraps" all nested Promise types:

\n\n
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;\n\ntype DeepPromise = Promise<Promise<Promise<number>>>;\ntype Unwrapped = UnwrapPromise<DeepPromise>; // number\n
\n\n

Notice the recursion — this is one of the most elegant advanced TypeScript techniques, allowing you to handle infinitely nested structures.

\n\n

3. Mapped Types: Transforming Objects

\n\n

Mapped types allow you to create new types based on existing ones by iterating over their keys. This is an indispensable tool for creating generic utilities:

\n\n
type Readonly<T> = {\n  readonly [K in keyof T]: T[K];\n};\n\ntype Partial<T> = {\n  [K in keyof T]?: T[K];\n};\n
\n\n

But we can go further. Let's create a type that makes all properties optional, but only if they are strings:

\n\n
type PartialStringProps<T> = {\n  [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];\n};\n\ninterface Config {\n  nam                

Blogs

Book Recommendations