Reflection and Introspection in TypeScript: How Code Looks at Itself

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

Reflection and Introspection in TypeScript: How Code Looks at Itself

\n\n

Imagine a program that can analyze itself at runtime. It can find out what types its variables have, what properties objects have, and even make decisions based on that information. Sounds like magic? In fact, this is reflection and introspection — powerful metaprogramming techniques that are also available in TypeScript.

\n\n

In this article, we'll figure out how reflection differs from introspection, how to use them in TypeScript, and why this can be useful in real projects. No boring theory — just live code and practical examples.

\n\n

What are Introspection and Reflection?

\n\n

Let's get one thing straight right away. In programming, these terms are often confused, but there is a difference:

\n\n
    \n
  • Introspection — is the ability of a program to observe its own structure. You can find out what properties an object has, what its type is, but you cannot change them.
  • \n
  • Reflection — is a more advanced level: the program not only looks at itself but also changes its behavior at runtime. For example, it dynamically calls methods or creates new instances of classes.
  • \n
\n\n

In JavaScript, on which TypeScript is based, reflection is implemented through Reflect and Proxy. But TypeScript adds its own layer — static typing, which partially "disappears" during compilation. This is where the most interesting part begins.

\n\n

Type Introspection: Type Guards and typeof

\n\n

The simplest way to look inside the code is to use type guards. These are constructs that check the data type at runtime:

\n\n
function isString(value: unknown): value is string {\n  return typeof value === 'string';\n}\n\nfunction processValue(value: unknown) {\n  if (isString(value)) {\n    // TypeScript knows that value is a string\n    console.log(value.toUpperCase());\n  } else {\n    console.log('This is not a string');\n  }\n}
\n\n

Notice value is string — this is a type predicate. It tells TypeScript that after the check, the variable belongs to a specific type. This is pure introspection: we look at the type, but we don't change it.

\n\n

Reflection via the Reflect API

\n\n

The Reflect object is a built-in reflection mechanism in JavaScript. It allows you to perform actions that were previously done with "magical" methods, but are now unified. Here are a few examples:

\n\n
const user = {\n  name: 'Alexey',\n  age: 30,\n};\n\n// Introspection — get the list of keys\nconsole.log(Reflect.ownKeys(user)); // ['name', 'age']\n\n// Reflection — dynamically read a property\nconst propertyName = 'name';\nconsole.log(Reflect.get(user, propertyName)); // 'Alexey'\n\n// Reflection — dynamically set a property\nReflect.set(user, 'name', 'Maria');\nconsole.log(user.name); // 'Maria'
\n\n

Why is Reflect better than the good old user[prop]? First, it returns a boolean for set operations, which is convenient for checks. Second, it works correctly with proxies and property descriptors. This is especially important for TypeScript, as reflection allows you to bypass static typing when it's really needed.

\n\n

Decorators: Reflection on Steroids

\n\n

Decorators are perhaps the most striking example of reflection in TypeScript. They allow you to annotate classes, methods, properties, and parameters, and then change their behavior. Since TypeScript 5.0, decorators have become standard (stage 3).

\n\n

Here's what a simple decorator for logging a method call looks like:

\n\n
function logMethod(\n  target: any,\n  propertyKey: string,\n  descriptor: PropertyDescriptor\n) {\n  const originalMethod = descriptor.valu                

Blogs

Book Recommendations