From 21285dd7a30afc2c9caf950023e4a19339b5f24e Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:55:27 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Only=20use=20the=20fallback=20pr?= =?UTF-8?q?ovider=20if=20`Reflect`=20has=20the=20whole=20metadata=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #165 When `Reflect.defineMetadata` exists but the registry doesn't, `CreateMetadataRegistry` wraps the existing `Reflect` in a fallback provider. The provider captures five methods, and `isProviderFor` calls `getOwnMetadataKeys` on every lookup. So if some other library put only part of the API on `Reflect`, every metadata lookup throws `TypeError: getOwnMetadataKeys is not a function`. To reproduce: 1. Install `reflect-metadata@0.2.2` and `@abraham/reflection`, which defines `defineMetadata` but not `getOwnMetadataKeys`. 2. Import `@abraham/reflection` before `reflect-metadata`. 3. Call `Reflect.getOwnMetadata("a", {})`. It throws the error above. With this change, the fallback is only used when all five methods it captures are functions. Otherwise `reflect-metadata` installs its own implementation, as it does when no metadata API is present. The trade-off is that metadata written through an incomplete implementation before `reflect-metadata` loads can't be read afterwards, which is better than failing on every lookup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Reflect.ts | 6 +++++- ReflectLite.ts | 6 +++++- ReflectNoConflict.ts | 6 +++++- test/registry.ts | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Reflect.ts b/Reflect.ts index b3468b8..dea1eb1 100644 --- a/Reflect.ts +++ b/Reflect.ts @@ -1497,7 +1497,11 @@ namespace Reflect { if (!IsUndefined(registrySymbol) && typeof root.Reflect !== "undefined" && !(registrySymbol in root.Reflect) && - typeof root.Reflect.defineMetadata === "function") { + typeof root.Reflect.defineMetadata === "function" && + typeof root.Reflect.hasOwnMetadata === "function" && + typeof root.Reflect.getOwnMetadata === "function" && + typeof root.Reflect.getOwnMetadataKeys === "function" && + typeof root.Reflect.deleteMetadata === "function") { // interoperate with older version of `reflect-metadata` that did not support a registry. fallback = CreateFallbackProvider(root.Reflect); } diff --git a/ReflectLite.ts b/ReflectLite.ts index 858da75..5e523e2 100644 --- a/ReflectLite.ts +++ b/ReflectLite.ts @@ -1457,7 +1457,11 @@ namespace Reflect { if (!IsUndefined(registrySymbol) && typeof root.Reflect !== "undefined" && !(registrySymbol in root.Reflect) && - typeof root.Reflect.defineMetadata === "function") { + typeof root.Reflect.defineMetadata === "function" && + typeof root.Reflect.hasOwnMetadata === "function" && + typeof root.Reflect.getOwnMetadata === "function" && + typeof root.Reflect.getOwnMetadataKeys === "function" && + typeof root.Reflect.deleteMetadata === "function") { // interoperate with older version of `reflect-metadata` that did not support a registry. fallback = CreateFallbackProvider(root.Reflect); } diff --git a/ReflectNoConflict.ts b/ReflectNoConflict.ts index f7707fd..0d50e76 100644 --- a/ReflectNoConflict.ts +++ b/ReflectNoConflict.ts @@ -1334,7 +1334,11 @@ function CreateMetadataRegistry(): MetadataRegistry { if (!IsUndefined(registrySymbol) && typeof Reflect !== "undefined" && !(registrySymbol in Reflect) && - typeof Reflect.defineMetadata === "function") { + typeof Reflect.defineMetadata === "function" && + typeof Reflect.hasOwnMetadata === "function" && + typeof Reflect.getOwnMetadata === "function" && + typeof Reflect.getOwnMetadataKeys === "function" && + typeof Reflect.deleteMetadata === "function") { // interoperate with older version of `reflect-metadata` that did not support a registry. fallback = CreateFallbackProvider(Reflect); } diff --git a/test/registry.ts b/test/registry.ts index 43ca659..8b9645e 100644 --- a/test/registry.ts +++ b/test/registry.ts @@ -115,5 +115,21 @@ for (const { name, header, context } of suites) { assert.isDefined(provider2); assert.strictEqual(provider1, provider2); }); + it("ignores incomplete implementation", () => { + const { missing, value } = script({ ...context })` + Reflect.defineMetadata = function() {}; + + { + ${header} + + const obj = {}; + exports.missing = Reflect.getOwnMetadata("a", obj); + Reflect.defineMetadata("a", 1, obj); + exports.value = Reflect.getOwnMetadata("a", obj); + } + `; + assert.isUndefined(missing); + assert.strictEqual(value, 1); + }); }); } \ No newline at end of file