diff --git a/packages/@ember/controller/index.ts b/packages/@ember/controller/index.ts index c45babc0ada..11686f5e150 100644 --- a/packages/@ember/controller/index.ts +++ b/packages/@ember/controller/index.ts @@ -366,3 +366,29 @@ export function inject( } export { Controller as default, ControllerMixin }; + +/** + A type registry for Ember `Controller`s. Meant to be declaration-merged so string + lookups resolve to the correct type. + + Blueprints should include such a declaration merge for TypeScript: + + ```ts + import Controller from '@ember/controller'; + + export default class ExampleController extends Controller { + // ... + } + + declare module '@ember/controller' { + export interface Registry { + example: ExampleController; + } + } + ``` + + Then `@inject` can check that the service is registered correctly, and APIs + like `owner.lookup('controller:example')` can return `ExampleController`. +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Registry extends Record {} diff --git a/packages/@ember/controller/owner-ext.d.ts b/packages/@ember/controller/owner-ext.d.ts new file mode 100644 index 00000000000..eb4c2b359e7 --- /dev/null +++ b/packages/@ember/controller/owner-ext.d.ts @@ -0,0 +1,11 @@ +// This module provides an 'extension' to the `@ember/owner` module from the +// `@ember/controller` module. Our type publishing infrastructure will pass it +// through unchanged, so end users will get this extension. + +import type { Registry } from '@ember/controller'; + +declare module '@ember/owner' { + export interface DIRegistry { + controller: Registry; + } +} diff --git a/packages/@ember/controller/type-tests/index.test.ts b/packages/@ember/controller/type-tests/index.test.ts index fdcca149715..35d37da6dbe 100644 --- a/packages/@ember/controller/type-tests/index.test.ts +++ b/packages/@ember/controller/type-tests/index.test.ts @@ -72,3 +72,14 @@ expectTypeOf(inject()).toMatchTypeOf(); // @ts-expect-error Doesn't allow invalid types inject(1); + +class ExampleController extends Controller {} + +declare module '@ember/controller' { + export interface Registry { + example: ExampleController; + } +} + +expectTypeOf(owner.lookup('controller:example')).toEqualTypeOf(); +expectTypeOf(owner.lookup('controller:non-registered')).toEqualTypeOf(); diff --git a/type-tests/@ember/controller-test/octane.ts b/type-tests/@ember/controller-test/octane.ts index ecb58c19dca..826dd56e0d3 100644 --- a/type-tests/@ember/controller-test/octane.ts +++ b/type-tests/@ember/controller-test/octane.ts @@ -1,4 +1,6 @@ import Controller, { ControllerQueryParam, inject } from '@ember/controller'; +import { expectTypeOf } from 'expect-type'; +import type Owner from '@ember/owner'; class FirstController extends Controller { foo = 'bar'; @@ -36,3 +38,9 @@ declare module '@ember/controller' { second: InstanceType; } } + +const owner = {} as Owner; + +expectTypeOf(owner.lookup('controller:first')).toEqualTypeOf(); +expectTypeOf(owner.lookup('controller:second')).toEqualTypeOf>(); +expectTypeOf(owner.lookup('controller:non-registered')).toEqualTypeOf();