This post takes a cursory look at reflection with TypeScript. Its primary focus is how reflection can be used with TypeScript decorators. It introduces Reflect
, reflect-metadata
, and some miscellaneous related components.
The Series so Far
- Decorator Introduction
- JavaScript Foundation
- Reflection
- Parameter Decorators
- Property Decorators
- Method Decorators
- Class Decorators
Eventual Topics:
- Where Decorators Work
- Decorating Instance Elements vs. Static Elements
- Examples
- Pairing Parameter Decorators with Method Decorators
- Pairing Property Decorators with Class Decorators
Code
You can view the code related to this post under the post-03-reflection
tag.
Overview
Reflection is the capacity of code to inspect and modify itself while running. Reflection implies (typically) that the code has a secondary interface with which to access everything. JavaScript’s eval
function is a great example; an arbitrary string is converted (hopefully) into meaningful elements and executed.
Reflect
ECMAScript 2015 added the Reflect
global. While might seem like a rehashed Object
, Reflect
adds some very useful reflection functionality.
ownKeys | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // This object will have prop = "cool" |
has | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // The visibility compiles out but whatever |
deleteProperty | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | (() => { |
emitDecoratorMetadata
TypeScript comes with a few experimental reflection features. As before, you’ll need to enable them first.
tsconfig.json | |
1 2 3 4 5 6 7 8 | { |
To investigate the experimental metadata, we’ll need to create a decorator. A logging method decorator was the first thing I typed out.
(Note: If you haven’t seen the previous post, you might benefit from a short skim.)
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function LogMethod( |
$ tsc --project tsconfig.json |
main.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
The __metadata
declaration is brand new. It looks similar to the __decorate
and __param
declarations that we’ve seen before. It too comes from deep in the compiler.
metadataHelper | |
1 2 3 | var __metadata = (this && this.__metadata) || function (k, v) { |
__metadata
attempts to create a Reflect.metadata
factory (more on that soon). It passes k
ey-v
alue pairs to Reflect.metadata
, which in turn stashes them away to be accessed later. By default, emitDecoratorMetadata
exposes three new properties:
design:type
: the type of the object being decorated (here aFunction
)design:paramtypes
: an array of types that match either the decorated item’s signature or its constructor’s signature (here[Number]
)design:returntype
: the return type of the object being decorated (herevoid 0
)
At the bottom of the file in the __decorate
call, you’ll see the __metadata
calls. Everything looks great. Except we’re missing an important component.
$ ts-node --print 'Reflect && Reflect.metadata || "whoops"' |
reflect-metadata
The reflect-metadata
package aims to extend the available reflection capabilities. While it is an experimental package, it comes recommended in the official docs and sees quite a bit of play. It’s also necessary to take advantage of the emitDecoratorMetadata
compiler option, as we’ve just discovered.
Defining new metadata is very easy. reflect-metadata
provides both imperative commands and a decorator factory. The decorator factory stores the metadata key-value pair and passes control through.
basic-usage.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import "reflect-metadata"; |
Accessing the data is also very easy. We can now grab the emitDecoratorMetadata
metadata that we’ve been trying to get to.
decorator-metadata.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import "reflect-metadata"; |
Example: Validate a Parameter Range
This group of files adds the ability to ensure specific parameters fall within a certain range. RANGE_KEY
is shared across files so everything can access the stashed ranges.
constants.ts | |
1 | export const RANGE_KEY = Symbol("validateRange"); |
When a parameter is decorated, add the range to the owning method’s metadata.
RangeParameter.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import "reflect-metadata"; |
This decorates the method that owns the decorated range. When called, it checks for any active ranges. Each watched parameter is checked against the range’s endpoints. An error is thrown if the value is out of the range.
ValidateRange.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import "reflect-metadata"; |
This puts everything together in a simple class.
Sample.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { RangeParameter } from "./RangeParameter"; |
This runs everything, illustrating how a successful update works and catching a failed update.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import "reflect-metadata"; |
Recap
Make sure to skim metadata coverage in the official docs. Reflection tweaks active code. Reflect
is a robust tool with applications outside this context. emitDecoratorMetadata
emits object type, param type, and return type, when reflect-metadata
is loaded. reflect-metadata
can easily link disparate portions of your app together via straightfoward key-value matching.
Legal
The TS logo is a modified @typescript
avatar; I turned the PNG into a vector. I couldn’t find the original and I didn’t see any licensing on the other art, so it’s most likely covered by the TypeScript project’s Apache 2.0 license. Any code from the TS project is similarly licensed.
If there’s a problem with anything, my email’s in the footer. Stay awesome.