Skip to content

emitDecoratorMetadata: design:type / design:paramtypes for builtin types (number, string, boolean, object) are the number 0, not the constructor #9501

Description

@proggeramlug

Found while verifying #9496 (the #9467 fix) against a tsc --experimentalDecorators --emitDecoratorMetadata + reflect-metadata oracle. Separate from #9467 — user-class types round-trip correctly; only builtin types are wrong.

design:type for a decorated property typed number / string / boolean is the number 0 (not the Number / String / Boolean constructor), object is undefined, and design:paramtypes entries for such parameters are the same 0. NestJS's injector reads design:paramtypes and compares entries against Number / String to tell a primitive-typed parameter from an injectable; class-transformer / class-validator read design:type to pick a coercion.

Repro

import "reflect-metadata";
function D(): any { return () => {}; }
class C {
  @D() n!: number;
  @D() s!: string;
  @D() b!: boolean;
  @D() o!: object;
  @D() m(x: number, y: string) {}
}
const n = Reflect.getMetadata("design:type", C.prototype, "n");
console.log("number:", typeof n, n === Number, n && (n as any).name);
const s = Reflect.getMetadata("design:type", C.prototype, "s");
console.log("string:", typeof s, s === String, s && (s as any).name);
const b = Reflect.getMetadata("design:type", C.prototype, "b");
console.log("boolean:", typeof b, b === Boolean, b && (b as any).name);
const o = Reflect.getMetadata("design:type", C.prototype, "o");
console.log("object:", typeof o, o === Object, o && (o as any).name);
const p = Reflect.getMetadata("design:paramtypes", C.prototype, "m");
console.log("params:", p.length, p[0] === Number, p[1] === String);
console.log("new via metadata:", n ? new (n as any)(5).valueOf() : "no ctor");

perry (main @ f1e9c37 + #9496, Linux x86-64):

number: number false 0
string: number false 0
boolean: number false 0
object: undefined false undefined
params: 2 false false
new via metadata: no ctor

node (tsc 5 + reflect-metadata 0.2):

number: function true Number
string: function true String
boolean: function true Boolean
object: function true Object
params: 2 true true
new via metadata: 5

Where

crates/perry-hir/src/lower/decorators.rs::type_metadata_expr maps Type::NumberExpr::ClassRef("Number"), Type::StringClassRef("String"), Type::BooleanClassRef("Boolean"), Type::ObjectClassRef("Object"), Type::FunctionClassRef("Function"), Type::ArrayClassRef("Array"). A ClassRef naming a builtin rather than a user class does not produce the global constructor value — the observed results are the number 0 (and undefined for Object). A user class (Type::Named) round-trips fine, which is why every existing decorator canary passes: they only ever compare against user classes.

Fix shape

Emit the global constructor value for builtin type names (the same value Number / String / … evaluate to as identifiers, e.g. through the global-builtin lookup the runtime already uses for Function in the class-ref constructor fallback), keep ClassRef for user classes, and pin the identities with a fixture whose expected output comes from the tsc oracle. Array, Function, Symbol, BigInt, Promise and a tuple / union / any (Object) deserve rows too.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions