Skip to content

Commit

Permalink
Introduce stable types for @ember/owner
Browse files Browse the repository at this point in the history
- Remove `@ember/-internals/owner` and `@ember/owner` from the list of
  excluded preview types in the types publishing script, so they now
  get emitted correctly into `types/stable`.

- Remove `@ember/owner` from the preview types and put it in the stable
  types instead, so users don't get conflicting type dependencies.

- Internally in `@ember/owner`, use absolute package paths, not
  relative. For referencing other (even internal) packages, it's
  important that we *not* use relative paths, so that (a) the published
  types work when wrapped in `declare module` statements but also (b)
  we have clearer boundaries for them, which will unlock further
  improvements to this infrastructure in the future.
  • Loading branch information
chriskrycho committed Nov 30, 2022
1 parent 86f6e52 commit 3ca9834
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 434 deletions.
4 changes: 2 additions & 2 deletions packages/@ember/owner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
// We need to provide a narrower public interface to `getOwner` so that we only
// expose the `Owner` type, *not* our richer `InternalOwner` type and its
// various bits of private API.
import Owner, { getOwner as internalGetOwner } from '../-internals/owner';
import Owner, { getOwner as internalGetOwner } from '@ember/-internals/owner';

// NOTE: this documentation appears here instead of at the definition site so
// it can appear correctly in both API docs and for TS, while providing a richer
Expand Down Expand Up @@ -99,4 +99,4 @@ export {
KnownForTypeResult,
Resolver,
DIRegistry,
} from '../-internals/owner';
} from '@ember/-internals/owner';
223 changes: 223 additions & 0 deletions type-tests/stable/@ember/owner-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import Owner, {
Factory,
FactoryManager,
FullName,
RegisterOptions,
Resolver,
KnownForTypeResult,
getOwner,
setOwner,
} from '@ember/owner';
import Component from '@glimmer/component';
import { expectTypeOf } from 'expect-type';
import {
getOwner as getOwnerApplication,
setOwner as setOwnerApplication,
} from '@ember/application';

expectTypeOf(getOwnerApplication).toEqualTypeOf(getOwner);
expectTypeOf(setOwnerApplication).toEqualTypeOf(setOwner);

// Just a class we can construct in the Factory and FactoryManager tests
declare class ConstructThis {
hasProps: boolean;
}

// ----- RegisterOptions ----- //
declare let regOptionsA: RegisterOptions;
expectTypeOf(regOptionsA.instantiate).toEqualTypeOf<boolean | undefined>();
expectTypeOf(regOptionsA.singleton).toEqualTypeOf<boolean | undefined>();

// ----- Factory ----- //
// This gives us coverage for the cases where you are *casting*.
declare let aFactory: Factory<ConstructThis>;

aFactory.create();
aFactory.create({});
aFactory.create({
hasProps: true,
});
aFactory.create({
hasProps: false,
});

// NOTE: it would be nice if these could be rejected by way of EPC, but alas: it
// cannot, because the public contract for `create` allows implementors to
// define their `create` config object basically however they like. :-/
aFactory.create({ unrelatedNonsense: 'yep yep yep' });
aFactory.create({ hasProps: true, unrelatedNonsense: 'yep yep yep' });

// But this should be legal.
const goodPojo = { hasProps: true, unrelatedNonsense: 'also true' };
aFactory.create(goodPojo);

// This should also be rejected, though for *type error* reasons, not EPC; alas,
// it cannot, for the same reason.
const badPojo = { hasProps: 'huzzah', unrelatedNonsense: 'also true' };
aFactory.create(badPojo);

// ----- FactoryManager ----- //
declare let aFactoryManager: FactoryManager<ConstructThis>;
expectTypeOf(aFactoryManager.class).toEqualTypeOf<Factory<ConstructThis>>();
expectTypeOf(aFactoryManager.create({})).toEqualTypeOf<ConstructThis>();
expectTypeOf(aFactoryManager.create({ hasProps: true })).toEqualTypeOf<ConstructThis>();
expectTypeOf(aFactoryManager.create({ hasProps: false })).toEqualTypeOf<ConstructThis>();

// Likewise with these.
aFactoryManager.create({ otherStuff: 'nope' });
aFactoryManager.create({ hasProps: true, otherStuff: 'nope' });
expectTypeOf(aFactoryManager.create(goodPojo)).toEqualTypeOf<ConstructThis>();
aFactoryManager.create(badPojo);

// ----- Resolver ----- //
declare let resolver: Resolver;
expectTypeOf<Resolver['normalize']>().toEqualTypeOf<
((fullName: FullName) => FullName) | undefined
>();
expectTypeOf<Resolver['lookupDescription']>().toEqualTypeOf<
((fullName: FullName) => string) | undefined
>();
expectTypeOf(resolver.resolve('random:some-name')).toEqualTypeOf<
object | Factory<object> | undefined
>();
const knownForFoo = resolver.knownForType?.('foo');
expectTypeOf(knownForFoo).toEqualTypeOf<KnownForTypeResult<'foo'> | undefined>();
expectTypeOf(knownForFoo?.['foo:bar']).toEqualTypeOf<boolean | undefined>();
// @ts-expect-error -- there is no `blah` on `knownForFoo`, *only* `foo`.
knownForFoo?.blah;

// This one is last so it can reuse the bits from above!
// ----- Owner ----- //
declare let owner: Owner;

// @ts-expect-error
owner.lookup();
expectTypeOf(owner.lookup('type:name')).toEqualTypeOf<unknown>();
// @ts-expect-error
owner.lookup('non-namespace-string');
expectTypeOf(owner.lookup('namespace@type:name')).toEqualTypeOf<unknown>();

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

expectTypeOf(owner.lookup('etc:my-type-test')).toEqualTypeOf<ConstructThis>();

expectTypeOf(owner.register('type:name', aFactory)).toEqualTypeOf<void>();
expectTypeOf(owner.register('type:name', aFactory, {})).toEqualTypeOf<void>();
expectTypeOf(owner.register('type:name', aFactory, { instantiate: true })).toEqualTypeOf<void>();
expectTypeOf(owner.register('type:name', aFactory, { instantiate: false })).toEqualTypeOf<void>();
expectTypeOf(owner.register('type:name', aFactory, { singleton: true })).toEqualTypeOf<void>();
expectTypeOf(owner.register('type:name', aFactory, { singleton: false })).toEqualTypeOf<void>();
expectTypeOf(
owner.register('type:name', aFactory, { instantiate: true, singleton: true })
).toEqualTypeOf<void>();
expectTypeOf(
owner.register('type:name', aFactory, { instantiate: true, singleton: false })
).toEqualTypeOf<void>();
expectTypeOf(
owner.register('type:name', aFactory, { instantiate: false, singleton: true })
).toEqualTypeOf<void>();
expectTypeOf(
owner.register('type:name', aFactory, { instantiate: false, singleton: false })
).toEqualTypeOf<void>();
// @ts-expect-error
owner.register('non-namespace-string', aFactory);
expectTypeOf(owner.register('namespace@type:name', aFactory)).toEqualTypeOf<void>();

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

// Tests deal with the fact that string literals are a special case! `let`
// bindings will accordingly not "just work" as a result. The separate
// assignments both satisfy the linter and show why it matters.
let aName;
aName = 'type:name';
// @ts-expect-error
owner.lookup(aName);

let aTypedName: FullName;
aTypedName = 'type:name';
expectTypeOf(owner.lookup(aTypedName)).toBeUnknown();

// Nor will callbacks work "out of the box". But they can work if they have the
// correct type.
declare const justStrings: string[];
// @ts-expect-error
justStrings.map((aString) => owner.lookup(aString));
declare let typedStrings: FullName[];
typedStrings.map((aString) => owner.lookup(aString));

// Also make sure it keeps working with const bindings
const aConstName = 'type:name';
expectTypeOf(owner.lookup(aConstName)).toBeUnknown();

// Check handling with Glimmer components carrying a Signature: they should
// properly resolve to `Owner`, *not* `Owner | undefined`.
interface Sig<T> {
Args: {
name: string;
age: number;
extra: T;
};
Element: HTMLParagraphElement;
Blocks: {
default: [greeting: string];
extra: [T];
};
}

class ExampleComponent<T> extends Component<Sig<T>> {
checkThis() {
expectTypeOf(getOwner(this)).toEqualTypeOf<Owner | undefined>();
}
}

declare let example: ExampleComponent<string>;
expectTypeOf(getOwner(example)).toEqualTypeOf<Owner | undefined>();

// ----- Minimal further coverage for POJOs ----- //
// `Factory` and `FactoryManager` don't have to deal in actual classes. :sigh:
const Creatable = {
hasProps: true,
};

const pojoFactory: Factory<typeof Creatable> = {
// If you want *real* safety here, alas: you cannot have it. The public
// contract for `create` allows implementors to define their `create` config
// object basically however they like. As a result, this is the safest version
// possible: Making it be `Partial<Thing>` is *compatible* with `object`, and
// requires full checking *inside* the function body. It does not, alas, give
// any safety *outside* the class. A future rationalization of this would be
// very welcome.
create(initialValues?: Partial<typeof Creatable>) {
const instance = Creatable;
if (initialValues) {
if (initialValues.hasProps) {
Object.defineProperty(instance, 'hasProps', {
value: initialValues.hasProps,
enumerable: true,
writable: true,
});
}
}
return instance;
},
};

expectTypeOf(pojoFactory.create()).toEqualTypeOf<{ hasProps: boolean }>();
2 changes: 2 additions & 0 deletions type-tests/stable/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This is equivalent to do `import 'ember-source/types';`.
import '../../types/stable';
3 changes: 3 additions & 0 deletions type-tests/stable/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@tsconfig/ember/tsconfig.json",
}
Loading

0 comments on commit 3ca9834

Please sign in to comment.