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::Number → Expr::ClassRef("Number"), Type::String → ClassRef("String"), Type::Boolean → ClassRef("Boolean"), Type::Object → ClassRef("Object"), Type::Function → ClassRef("Function"), Type::Array → ClassRef("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.
Found while verifying #9496 (the #9467 fix) against a
tsc --experimentalDecorators --emitDecoratorMetadata+reflect-metadataoracle. Separate from #9467 — user-class types round-trip correctly; only builtin types are wrong.design:typefor a decorated property typednumber/string/booleanis the number0(not theNumber/String/Booleanconstructor),objectisundefined, anddesign:paramtypesentries for such parameters are the same0. NestJS's injector readsdesign:paramtypesand compares entries againstNumber/Stringto tell a primitive-typed parameter from an injectable; class-transformer / class-validator readdesign:typeto pick a coercion.Repro
perry (main @ f1e9c37 + #9496, Linux x86-64):
node (tsc 5 + reflect-metadata 0.2):
Where
crates/perry-hir/src/lower/decorators.rs::type_metadata_exprmapsType::Number→Expr::ClassRef("Number"),Type::String→ClassRef("String"),Type::Boolean→ClassRef("Boolean"),Type::Object→ClassRef("Object"),Type::Function→ClassRef("Function"),Type::Array→ClassRef("Array"). AClassRefnaming a builtin rather than a user class does not produce the global constructor value — the observed results are the number0(andundefinedforObject). 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 forFunctionin the class-refconstructorfallback), keepClassReffor user classes, and pin the identities with a fixture whose expected output comes from the tsc oracle.Array,Function,Symbol,BigInt,Promiseand a tuple / union /any(Object) deserve rows too.