Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions types/ember__owner/ember__owner-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,25 @@ owner.register('type:name', aFactory, { instantiate: false, singleton: false });
owner.register('non-namespace-string', aFactory);
owner.register('namespace@type:name', aFactory); // $ExpectType void

owner.factoryFor('type:name'); // $ExpectType FactoryManager<unknown> | undefined
owner.factoryFor('type:name')?.class; // $ExpectType Factory<unknown> | undefined
owner.factoryFor('type:name')?.create(); // $ExpectType unknown
owner.factoryFor('type:name')?.create({}); // $ExpectType unknown
owner.factoryFor('type:name')?.create({ anythingGoes: true }); // $ExpectType unknown
owner.factoryFor('type:name'); // $ExpectType FactoryManager<object> | undefined
owner.factoryFor('type:name')?.class; // $ExpectType Factory<object> | undefined
owner.factoryFor('type:name')?.create(); // $ExpectType object | undefined
owner.factoryFor('type:name')?.create({}); // $ExpectType object | undefined
owner.factoryFor('type:name')?.create({ anythingGoes: true }); // $ExpectType object | undefined
// @ts-expect-error
owner.factoryFor('non-namespace-string');
owner.factoryFor('namespace@type:name'); // $ExpectType FactoryManager<unknown> | undefined
owner.factoryFor('namespace@type:name'); // $ExpectType FactoryManager<object> | undefined

// Arbitrary registration patterns work, as here.
declare module '@ember/owner' {
interface DIRegistry {
etc: {
'my-type-test': ConstructThis;
};
}
}

owner.lookup('etc:my-type-test'); // $ExpectType ConstructThis

// Tests deal with the fact that string literals are a special case! `let`
// bindings will accordingly not "just work" as a result. The separate
Expand Down
81 changes: 73 additions & 8 deletions types/ember__owner/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,71 @@
* The name for a factory consists of a namespace and the name of a specific
* type within that namespace, like `'service:session'`.
*/
export type FullName = `${string}:${string}`;
export type FullName<Type extends string = string, Name extends string = string> = `${Type}:${Name}`;

/**
* A type registry for the DI system, which other participants in the DI system
* can register themselves into with declaration merging. The contract for this
* type is that its keys are the `Type` from a `FullName`, and each value for a
* `Type` is another registry whose keys are the `Name` from a `FullName`. The
* mechanic for providing a registry is [declaration merging][handbook].
*
* [handbook]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
*
* For example, Ember's `@ember/service` module includes this set of definitions:
*
* ```ts
* export default class Service extends EmberObject {}
*
* // For concrete singleton classes to be merged into.
* interface Registry extends Record<string, Service> {}
*
* declare module '@ember/owner' {
* service: Registry;
* }
* ```
*
* Declarations of services can then include the registry:
*
* ```ts
* import Service from '@ember/service';
*
* export default class Session extends Service {
* login(username: string, password: string) {
* // ...
* }
* }
*
* declare module '@ember/service' {
* interface Registry {
* session: Session;
* }
* }
* ```
*
* Then users of the `Owner` API will be able to do things like this with strong
* type safety guarantees:
*
* ```ts
* getOwner(this)?.lookup('service:session').login("hello", "1234abcd");
* ```
*
* @internal
*/
export interface DIRegistry extends Record<string, Record<string, unknown>> {}

type ValidType = keyof DIRegistry & string;
type ValidName<Type extends ValidType> = keyof DIRegistry[Type] & string;

type ResolveFactoryManager<
Type extends string,
Name extends string,
> = DIRegistry[Type][Name] extends infer RegistryEntry
? RegistryEntry extends object
? FactoryManager<RegistryEntry>
: FactoryManager<object> | undefined
: never;

// TODO: when migrating into Ember proper, evaluate whether we should introduce
// a registry which users can provide to resolve known types, so e.g.
// `owner.lookup('service:session')` can return the right thing.
/**
* Framework objects in an Ember application (components, services, routes,
* etc.) are created via a factory and dependency injection system. Each of
Expand All @@ -26,7 +86,10 @@ export default interface Owner {
/**
* Given a {@linkcode FullName} return a corresponding instance.
*/
lookup(fullName: FullName): unknown;
lookup<Type extends ValidType, Name extends ValidName<Type>>(
fullName: FullName<Type, Name>,
options?: RegisterOptions,
): DIRegistry[Type][Name];

/**
* Registers a factory or value that can be used for dependency injection
Expand All @@ -39,7 +102,7 @@ export default interface Owner {
* - To override the default singleton behavior and instead create multiple
* instances, pass the `{ singleton: false }` option.
*/
// Dear future maintainer: yes, I know that `Factory<unknown> | object` is
// Dear future maintainer: yes, I know that `Factory<object> | object` is
// an exceedingly weird type here. This is how we type it internally in
// Ember itself. We actually allow more or less *anything* to be passed
// here. In the future, we may possibly be able to update this to actually
Expand All @@ -48,7 +111,7 @@ export default interface Owner {
// factory, not needing `create` if `options.instantiate` is `false`, etc.)
// but doing so will require rationalizing Ember's own internals and may
// need a full Ember RFC.
register(fullName: FullName, factory: Factory<unknown> | object, options?: RegisterOptions): void;
register(fullName: FullName, factory: Factory<object> | object, options?: RegisterOptions): void;

/**
* Given a fullName of the form `'type:name'`, like `'route:application'`,
Expand All @@ -58,7 +121,9 @@ export default interface Owner {
* destroyed manually by the caller of `.create()`. Typically, this is done
* during the creating objects own `destroy` or `willDestroy` methods.
*/
factoryFor(fullName: FullName): FactoryManager<unknown> | undefined;
factoryFor<Type extends ValidType, Name extends ValidName<Type>>(
fullName: FullName<Type, Name>,
): ResolveFactoryManager<Type, Name>;
}

export interface RegisterOptions {
Expand Down
8 changes: 7 additions & 1 deletion types/ember__service/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ export function service<K extends keyof Registry>(name: K): ComputedProperty<Reg
// A type registry for Ember `Service`s. Meant to be declaration-merged so
// string lookups resolve to the correct type.
// tslint:disable-next-line no-empty-interface strict-export-declare-modifiers
interface Registry {}
interface Registry extends Record<string, Service> {}

declare module '@ember/owner' {
interface DIRegistry {
service: Registry;
}
}