Reflection and Introspection in TypeScript: How Code Looks at Itself
\n\nImagine 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\nIn 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\nWhat are Introspection and Reflection?
\n\nLet'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
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.
Type Introspection: Type Guards and typeof
\n\nThe simplest way to look inside the code is to use type guards. These are constructs that check the data type at runtime:
\n\nfunction 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\nNotice 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.
Reflection via the Reflect API
\n\nThe 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:
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\nWhy 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.
Decorators: Reflection on Steroids
\n\nDecorators 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\nHere's what a simple decorator for logging a method call looks like:
\n\nfunction logMethod(\n target: any,\n propertyKey: string,\n descriptor: PropertyDescriptor\n) {\n const originalMethod = descriptor.valu