From 213ebe77cc6c92191f1e19e7918e282c1a8c4d30 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 13 Apr 2026 19:48:42 +0200 Subject: [PATCH 1/5] Harmonize phantom .T getter behavior between ExtensionPoint and ServiceRef Both `createExtensionPoint` and `createServiceRef` now consistently return `null` from the phantom `.T` getter instead of throwing. Added `toJSON()` to `ExtensionPoint` for parity with `ServiceRef`. Signed-off-by: Patrik Oldsberg Made-with: Cursor Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/harmonize-phantom-t-getter.md | 5 +++++ .../src/services/system/types.test.ts | 12 ++++++++++++ .../backend-plugin-api/src/services/system/types.ts | 3 +-- .../src/wiring/createExtensionPoint.test.ts | 4 ++-- .../src/wiring/createExtensionPoint.ts | 6 +----- 5 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 .changeset/harmonize-phantom-t-getter.md diff --git a/.changeset/harmonize-phantom-t-getter.md b/.changeset/harmonize-phantom-t-getter.md new file mode 100644 index 0000000000..970d4f18b2 --- /dev/null +++ b/.changeset/harmonize-phantom-t-getter.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Harmonized the phantom `.T` getter on `ExtensionPoint` to consistently return `null` instead of throwing. diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts index a98b8c44ee..be588f2036 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -25,6 +25,18 @@ const rootDep = createServiceRef({ id: 'y', scope: 'root' }); const pluginDep = createServiceRef({ id: 'z' }); function unused(..._any: any[]) {} +describe('createServiceRef', () => { + it('should create a ServiceRef', () => { + expect(ref.id).toBe('x'); + expect(ref.scope).toBe('plugin'); + expect(ref.T).toBe(null); + expect(String(ref)).toBe('serviceRef{x}'); + expect(JSON.stringify(ref)).toBe( + '{"$$type":"@backstage/ServiceRef","id":"x","scope":"plugin","multiton":false}', + ); + }); +}); + describe('createServiceFactory', () => { it('should create a plugin scoped factory', () => { const factory = createServiceFactory({ diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index c6425cca42..925156d3ae 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -144,13 +144,12 @@ export function createServiceRef< scope, multiton, get T(): TService { - throw new Error(`tried to read ServiceRef.T of ${this}`); + return null as TService; }, toString() { return `serviceRef{${options.id}}`; }, toJSON() { - // This avoids accidental calls to T happening e.g. in tests return { $$type: '@backstage/ServiceRef', id, diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts index 825063e845..df29833432 100644 --- a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts @@ -18,10 +18,10 @@ import { createExtensionPoint } from './createExtensionPoint'; describe('createExtensionPoint', () => { it('should create an ExtensionPoint', () => { - const extensionPoint = createExtensionPoint({ id: 'x' }); + const extensionPoint = createExtensionPoint({ id: 'x' }); expect(extensionPoint).toBeDefined(); expect(extensionPoint.id).toBe('x'); - expect(() => extensionPoint.T).not.toThrow(); + expect(extensionPoint.T).toBe(null); expect(String(extensionPoint)).toBe('extensionPoint{x}'); }); }); diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts index 9dbae249ea..a68196f8c8 100644 --- a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts @@ -44,11 +44,7 @@ export function createExtensionPoint( return { id: options.id, get T(): T { - if (process.env.NODE_ENV === 'test') { - // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken - return null as T; - } - throw new Error(`tried to read ExtensionPoint.T of ${this}`); + return null as T; }, toString() { return `extensionPoint{${options.id}}`; From d79072b5de4cfe5a3029886d5a00326df392df23 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 13 Apr 2026 23:10:16 +0200 Subject: [PATCH 2/5] Update phantom T getter doc comments and expand changeset scope Updated doc comments on ExtensionPoint.T and ServiceRef.T to reflect that reading the value returns null rather than throwing. Expanded the changeset to cover all three behavioral changes in the PR. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/harmonize-phantom-t-getter.md | 2 +- packages/backend-plugin-api/src/services/system/types.ts | 2 +- packages/backend-plugin-api/src/wiring/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/harmonize-phantom-t-getter.md b/.changeset/harmonize-phantom-t-getter.md index 970d4f18b2..99995cd8e2 100644 --- a/.changeset/harmonize-phantom-t-getter.md +++ b/.changeset/harmonize-phantom-t-getter.md @@ -2,4 +2,4 @@ '@backstage/backend-plugin-api': patch --- -Harmonized the phantom `.T` getter on `ExtensionPoint` to consistently return `null` instead of throwing. +Harmonized the phantom `.T` getter behavior on `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing, and added `toJSON()` parity for `ExtensionPoint`. diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 925156d3ae..a97d577ec4 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -50,7 +50,7 @@ export type ServiceRef< /** * Utility for getting the type of the service, using `typeof serviceRef.T`. - * Attempting to actually read this value will result in an exception. + * Reading this value will always return `null`. It is only intended for use with `typeof serviceRef.T`. */ T: TService; diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 9397cf597b..8d75c8923f 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -27,7 +27,7 @@ export type ExtensionPoint = { /** * Utility for getting the type of the extension point, using `typeof extensionPoint.T`. - * Attempting to actually read this value will result in an exception. + * Reading this value will always return `null`. It is only intended for use with `typeof extensionPoint.T`. */ T: T; From ad33d56c9edec3fa20f542c0cd303b73523337e9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 13 Apr 2026 23:43:35 +0200 Subject: [PATCH 3/5] Update changeset wording Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/harmonize-phantom-t-getter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/harmonize-phantom-t-getter.md b/.changeset/harmonize-phantom-t-getter.md index 99995cd8e2..88e0bff995 100644 --- a/.changeset/harmonize-phantom-t-getter.md +++ b/.changeset/harmonize-phantom-t-getter.md @@ -2,4 +2,4 @@ '@backstage/backend-plugin-api': patch --- -Harmonized the phantom `.T` getter behavior on `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing, and added `toJSON()` parity for `ExtensionPoint`. +Aligned `.T` behavior between `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing, and added `toJSON()` parity for `ExtensionPoint`. From 47ca9472790bd3ab58f3bffb9e5108b4748b61c9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 13 Apr 2026 23:48:27 +0200 Subject: [PATCH 4/5] Fix changeset wording to remove incorrect toJSON mention Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/harmonize-phantom-t-getter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/harmonize-phantom-t-getter.md b/.changeset/harmonize-phantom-t-getter.md index 88e0bff995..566d889984 100644 --- a/.changeset/harmonize-phantom-t-getter.md +++ b/.changeset/harmonize-phantom-t-getter.md @@ -2,4 +2,4 @@ '@backstage/backend-plugin-api': patch --- -Aligned `.T` behavior between `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing, and added `toJSON()` parity for `ExtensionPoint`. +Aligned `.T` behavior between `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing. From c40568f651f32d47b7d892fd1204749a96dc80c7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 14 Apr 2026 00:06:53 +0200 Subject: [PATCH 5/5] Simplify phantom T props to plain null values instead of getters Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/backend-plugin-api/src/services/system/types.ts | 4 +--- .../backend-plugin-api/src/wiring/createExtensionPoint.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index a97d577ec4..cc4c28c8c1 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -143,9 +143,7 @@ export function createServiceRef< id, scope, multiton, - get T(): TService { - return null as TService; - }, + T: null as TService, toString() { return `serviceRef{${options.id}}`; }, diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts index a68196f8c8..7f8a45ff5a 100644 --- a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts @@ -43,9 +43,7 @@ export function createExtensionPoint( ): ExtensionPoint { return { id: options.id, - get T(): T { - return null as T; - }, + T: null as T, toString() { return `extensionPoint{${options.id}}`; },