A TypeScript service uses experimentalDecorators and emitDecoratorMetadata, and its DI container resolves constructor dependencies from design:paramtypes. One parameter is typed as an interface and the container fails to resolve it. Why does the metadata record `Object` there, and how do you make that dependency injectable?
answer
- what value could it possibly point at?
- one token, shared by everything unrepresentable
- a class has runtime identity, a shape does not
- abstract class as the contract
- or name the token yourself
basics
~20 sAn interface has no runtime value, so the compiler serialises the parameter to Object and the container has no token to look up. Fix it by depending on a class or abstract class, or by supplying an explicit injection token through a parameter decorator.
solid answer
~50 s`design:paramtypes` holds runtime *values*, and the compiler can only emit a value for a type that has one. A class type serialises to its constructor; an interface, a type alias for an object shape, a union, or a generic type parameter has nothing to point at, so the compiler emits `Object`. The container then sees `Object` as the token for that slot and has no registration under it — hence the resolution failure. There are two standard fixes. Depend on a concrete or abstract class instead of an interface, so the emitted metadata is a real constructor the container can key on. Or keep the interface as the compile-time type and attach an explicit token with a parameter decorator (`@Inject(LOGGER)`), which writes its own metadata that overrides the useless `Object`. The second is what every mature container does for non-class dependencies such as config objects and primitives.
code
typescript · 20 linesimport "reflect-metadata";
interface Logger { log(msg: string): void; }
class Clock { now(): Date { return new Date(); } }
function Injectable(): ClassDecorator {
return () => {};
}
@Injectable()
class Service {
constructor(
private readonly logger: Logger,
private readonly clock: Clock,
) {}
}
const types: Function[] = Reflect.getMetadata("design:paramtypes", Service);
console.log(types.map((t) => t.name));
// [ "Object", "Clock" ]go deeper
Know that interfaces disappear when TypeScript compiles, so no runtime tool can look one up by name. That single fact explains the failure even before you know the container's mechanics.
Explain the serialisation rule: class types become their constructors, primitives become wrapper constructors, and anything without a runtime value becomes Object — which is not a usable lookup key.
Show the two fixes and when each applies: an abstract class when a runtime identity is acceptable, an explicit token whenever the dependency is a config object, a primitive, or one of several generic instantiations.
Frame it as a design constraint on the dependency graph: metadata-driven wiring is only as precise as the serialisation, so decide as a policy where the codebase uses class tokens and where it uses declared tokens, rather than discovering it per failure.
## The mechanism, restated precisely With `experimentalDecorators` and `emitDecoratorMetadata` on, a decorated class gets a `design:paramtypes` entry holding an array of runtime values, one per constructor parameter. The compiler builds that array by *serialising* each declared parameter type: it emits a reference to whatever value corresponds to the type. The rule is simple and brutal. If the type is a class, the value is that class's constructor. If it is `string`, `number`, or `boolean`, the value is `String`, `Number`, or `Boolean`. If it is an array type, the value is `Array`. For everything else — an interface, a type alias for an object literal shape, a union of unrelated types, `any`, `unknown`, a bare type parameter — there is nothing in the running program to reference, and the compiler emits `Object`. ```typescript import "reflect-metadata"; interface Logger { log(msg: string): void; } class Clock { now() { return new Date(); } } function Injectable(): ClassDecorator { return () => {}; } @Injectable() class Service { constructor(private logger: Logger, private clock: Clock) {} } const types: Function[] = Reflect.getMetadata("design:paramtypes", Service); console.log(types.map((t) => t.name)); // [ "Object", "Clock" ] ``` ## Why the container fails the way it does A metadata-driven container uses each entry as a lookup key. `Clock` is a fine key — you registered a provider under that class. `Object` is not: it is not a thing anyone registers, and worse, *every* unrepresentable parameter in the whole application serialises to the same `Object`, so the key is not even unique. The container reports something like "cannot resolve parameter 0", and the cause is a type-level fact (interfaces are erased) surfacing as a runtime configuration error. This is the single most instructive consequence of erasure in a decorator codebase, which is why it is asked. The candidate who says "interfaces do not exist at runtime" has the answer; the candidate who starts editing container configuration does not. ## Generics lose their arguments too A related trap: a parameter typed `Repository<User>` does *not* serialise to `Object`. It serialises to the `Repository` constructor, with the type argument dropped, because the compiler serialises the type *reference* and generics are erased. So a container asked for `Repository<User>` and one asked for `Repository<Post>` see the identical token and get the identical provider. Frameworks work around this with an explicit token per entity rather than trying to recover the type argument, because the information genuinely is not there. ## Fix one: depend on a class Replace the interface with a class — often an abstract class that declares the contract and is never instantiated directly: ```typescript abstract class Logger { abstract log(msg: string): void; } class ConsoleLogger extends Logger { log(msg: string) { console.log(msg); } } ``` Now `design:paramtypes` records `Logger`, a real constructor, and the container registers `ConsoleLogger` as the provider for the `Logger` token. You keep a compile-time contract and gain a runtime identity. The price is that the abstraction is now a class in your emitted bundle rather than free type-level structure, and consumers who wanted structural typing must actually extend it. ## Fix two: an explicit token Keep the interface and stop relying on serialisation. Declare a token — a unique symbol or a const string — register the provider under it, and annotate the parameter with a decorator that records the token in its own metadata (`@Inject(LOGGER)` in the frameworks that spell it that way). A container always prefers an explicit token over the serialised type, so the `Object` entry becomes irrelevant. This is the only option for dependencies with no class at all: a config object, a connection string, a feature-flag map. It also makes the wiring visible at the call site, which is a readability gain some teams prefer even where a class would work. ## The judgment to bring The framing that lands in an interview is: *metadata-driven DI can only be as precise as the type serialisation, and the serialisation is deliberately shallow.* Design your dependency graph around types that have runtime identity, and use explicit tokens the moment they do not. Treat "my interface dependency will not inject" as a design signal, not a bug to configure around — and be aware that this whole mechanism is confined to the `experimentalDecorators` world, since standard decorators receive no `design:paramtypes` at all.
- Why does depending on an abstract class solve this when depending on an interface does not?An abstract class emits a real constructor function even though it can never be instantiated, so `design:paramtypes` records that constructor and the container has a unique key to register against. An interface emits nothing at all, leaving only the `Object` fallback. You are trading structural typing for runtime identity, deliberately.
- A parameter typed `Repository<User>` and one typed `Repository<Post>` inject the same provider. Why?Serialisation keeps the type reference and drops the type arguments, so both record the `Repository` constructor and the container sees one token. The type argument has no runtime representation to recover. The remedy is an explicit token per concrete repository, registered separately.
- How would you catch this class of bug before it reaches runtime?Nothing in the type system flags it, so lean on tooling and convention: a container that validates its graph at startup rather than lazily, a lint rule or review convention that constructor dependencies must be classes or carry an explicit token, and an integration test that instantiates the root module so unresolved tokens fail the build, not a request.
saying these in an interview costs you the question
- Thinks the container can read the interface name
- Blames the container configuration rather than erasure
- Says adding a decorator to the interface would help
- Believes generic type arguments survive into paramtypes
- Suggests a runtime instanceof check against the interface