Merge pull request #33878 from backstage/rugvip/harmonize-phantom-t-getter

backend-plugin-api: harmonize phantom .T getter behavior
This commit is contained in:
Patrik Oldsberg
2026-04-14 00:54:04 +02:00
committed by GitHub
6 changed files with 23 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Aligned `.T` behavior between `ExtensionPoint` and `ServiceRef` to consistently return `null` instead of throwing.
@@ -25,6 +25,18 @@ const rootDep = createServiceRef<number>({ id: 'y', scope: 'root' });
const pluginDep = createServiceRef<boolean>({ 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({
@@ -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;
@@ -143,14 +143,11 @@ export function createServiceRef<
id,
scope,
multiton,
get T(): TService {
throw new Error(`tried to read ServiceRef.T of ${this}`);
},
T: 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,
@@ -18,10 +18,10 @@ import { createExtensionPoint } from './createExtensionPoint';
describe('createExtensionPoint', () => {
it('should create an ExtensionPoint', () => {
const extensionPoint = createExtensionPoint({ id: 'x' });
const extensionPoint = createExtensionPoint<string>({ 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}');
});
});
@@ -43,13 +43,7 @@ export function createExtensionPoint<T>(
): ExtensionPoint<T> {
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}`);
},
T: null as T,
toString() {
return `extensionPoint{${options.id}}`;
},
@@ -27,7 +27,7 @@ export type ExtensionPoint<T> = {
/**
* 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;