Skip to content

Commit

Permalink
Future public types for @ember/component
Browse files Browse the repository at this point in the history
  • Loading branch information
wagenet committed Feb 18, 2022
1 parent 8388b3d commit 4b1ccc2
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 11 deletions.
27 changes: 20 additions & 7 deletions packages/@ember/-internals/glimmer/lib/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import { consumeTag, createTag, dirtyTag } from '@glimmer/validator';

export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG');

export type HelperFunction<T = unknown> = (positional: unknown[], named: Dict<unknown>) => T;
export type HelperFunction<
T = unknown,
P extends unknown[] = unknown[],
N extends Dict<unknown> = Dict<unknown>
> = (positional: P, named: N) => T;

export type SimpleHelperFactory = Factory<SimpleHelper, HelperFactory<SimpleHelper>>;
export type ClassHelperFactory = Factory<HelperInstance, HelperFactory<HelperInstance>>;
Expand All @@ -30,8 +34,12 @@ export interface HelperInstance<T = unknown> {

const IS_CLASSIC_HELPER: unique symbol = Symbol('IS_CLASSIC_HELPER');

export interface SimpleHelper<T = unknown> {
compute: HelperFunction<T>;
export interface SimpleHelper<
T = unknown,
P extends unknown[] = unknown[],
N extends Dict<unknown> = Dict<unknown>
> {
compute: HelperFunction<T, P, N>;
}

/**
Expand Down Expand Up @@ -89,7 +97,7 @@ interface Helper {
@public
@since 1.13.0
*/
compute(params: unknown[], hash: object): unknown;
compute(params: unknown[], hash: Dict<unknown>): unknown;
}
class Helper extends FrameworkObject {
static isHelperFactory = true;
Expand Down Expand Up @@ -199,10 +207,11 @@ export const CLASSIC_HELPER_MANAGER = getInternalHelperManager(Helper);

///////////

class Wrapper implements HelperFactory<SimpleHelper> {
class Wrapper<T = unknown, P extends unknown[] = unknown[], N extends Dict<unknown> = Dict<unknown>>
implements HelperFactory<SimpleHelper<T, P, N>> {
isHelperFactory: true = true;

constructor(public compute: HelperFunction) {}
constructor(public compute: HelperFunction<T, P, N>) {}

create() {
// needs new instance or will leak containers
Expand Down Expand Up @@ -256,7 +265,11 @@ setHelperManager(() => SIMPLE_CLASSIC_HELPER_MANAGER, Wrapper.prototype);
@public
@since 1.13.0
*/
export function helper(helperFn: HelperFunction): HelperFactory<SimpleHelper> {
export function helper<
T = unknown,
P extends unknown[] = unknown[],
N extends Dict<unknown> = Dict<unknown>
>(helperFn: HelperFunction<T, P, N>): HelperFactory<SimpleHelper<T, P, N>> {
return new Wrapper(helperFn);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/@ember/-internals/glimmer/lib/utils/managers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import {
@return {Object} the same object passed in
@public
*/
export function setComponentManager(
export function setComponentManager<T extends object>(
manager: (owner: Owner) => ComponentManager<unknown>,
obj: object
): object {
obj: T
): T {
return glimmerSetComponentManager(manager, obj);
}

Expand Down
16 changes: 16 additions & 0 deletions packages/@ember/component/type-tests/capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { capabilities } from '@ember/component';
import { expectTypeOf } from 'expect-type';

expectTypeOf(capabilities('3.13')).toMatchTypeOf<{
asyncLifecycleCallbacks?: boolean | undefined;
destructor?: boolean | undefined;
updateHook?: boolean | undefined;
}>();

capabilities('3.13', { asyncLifecycleCallbacks: true });
capabilities('3.4', { asyncLifecycleCallbacks: true });

// @ts-expect-error invalid capabilities
capabilities('3.13', { asyncLifecycleCallbacks: 1 });
// @ts-expect-error invalid verison
capabilities('3.12');
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getComponentTemplate } from '@ember/component';
import { TemplateFactory } from '@glimmer/interfaces';
import { expectTypeOf } from 'expect-type';

expectTypeOf(getComponentTemplate({})).toEqualTypeOf<TemplateFactory | undefined>();

// @ts-expect-error requires param
getComponentTemplate();
18 changes: 18 additions & 0 deletions packages/@ember/component/type-tests/helper/helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HelperFactory, SimpleHelper } from '@ember/-internals/glimmer/lib/helper';
import { helper } from '@ember/component/helper';
import { expectTypeOf } from 'expect-type';

// NOTE: The types for `helper` are not actually safe. Glint helps with this.

let myHelper = helper(function ([cents]: [number], { currency }: { currency: string }) {
return `${currency}${cents * 0.01}`;
});
expectTypeOf(myHelper).toEqualTypeOf<
HelperFactory<SimpleHelper<string, [number], { currency: string }>>
>();

// @ts-expect-error invalid named params
helper(function ([cents]: [number], named: number) {});

// @ts-expect-error invalid params
helper(function (params: number) {});
42 changes: 42 additions & 0 deletions packages/@ember/component/type-tests/helper/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Owner } from '@ember/-internals/owner';
import { FrameworkObject } from '@ember/-internals/runtime';
import Helper from '@ember/component/helper';
import { expectTypeOf } from 'expect-type';

// Good enough for tests
let owner = {} as Owner;

// NOTE: The types for `compute` are not actually safe. Glint helps with this.

let helper = new Helper(owner);

expectTypeOf(helper).toMatchTypeOf<FrameworkObject>();

class MyHelper extends Helper {
compute([cents]: [number], { currency }: { currency: string }) {
return `${currency}${cents * 0.01}`;
}
}
new MyHelper(owner);

class NoHash extends Helper {
compute([cents]: [number]): string {
return `${cents * 0.01}`;
}
}
new NoHash(owner);

class NoParams extends Helper {
compute(): string {
return 'hello';
}
}
new NoParams(owner);

class InvalidHelper extends Helper {
// @ts-expect-error Invalid params
compute(value: boolean): string {
return String(value);
}
}
new InvalidHelper(owner);
41 changes: 41 additions & 0 deletions packages/@ember/component/type-tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,50 @@ import Component from '@ember/component';
import { CoreView } from '@ember/-internals/views';
import { expectTypeOf } from 'expect-type';
import { Owner } from '@ember/-internals/owner';
import { View } from '@ember/-internals/glimmer/lib/renderer';
import { action } from '@ember/object';
import { tracked } from '@ember/-internals/metal';

// NOTE: This is invalid, but acceptable for type tests
let owner = {} as Owner;
let component = new Component(owner);

expectTypeOf(component).toMatchTypeOf<CoreView>();
expectTypeOf(component).toMatchTypeOf<View>();

class MyComponent extends Component {
tagName = 'em';
classNames = ['my-class', 'my-other-class'];
classNameBindings = ['propertyA', 'propertyB'];
attributeBindings = ['href'];

@tracked propertyA = 'from-a';

get propertyB(): string | void {
if (this.propertyA === 'from-a') {
return 'from-b';
}
}

@tracked href = 'https://tilde.io';

@action click(_event: Event): void {
// Clicked!
}
}
new MyComponent(owner);

class BadComponent extends Component {
// @ts-expect-error invalid tag name
tagName = 1;

// @ts-expect-error invalid classname
classNames = 'foo';

// @ts-expect-error invalid classNameBindings
classNameBindings = [1];

// @ts-expect-error invalid attributeBindings
attributeBindings = [true];
}
new BadComponent(owner);
17 changes: 17 additions & 0 deletions packages/@ember/component/type-tests/set-component-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Owner } from '@ember/-internals/owner';
import { setComponentManager } from '@ember/component';
import { ComponentManager } from '@glimmer/interfaces';
import { expectTypeOf } from 'expect-type';

// Obviously this is invalid, but it works for our purposes.
let manager = {} as ComponentManager<unknown>;

class Foo {}
let foo = new Foo();

expectTypeOf(setComponentManager((_owner: Owner) => manager, foo)).toEqualTypeOf<Foo>();

// @ts-expect-error invalid callback
setComponentManager(() => {
return {};
}, foo);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { setComponentTemplate } from '@ember/component';
import { TemplateFactory } from '@glimmer/interfaces';
import { expectTypeOf } from 'expect-type';

// Good enough for testing
let factory = {} as TemplateFactory;

expectTypeOf(setComponentTemplate(factory, {})).toEqualTypeOf<object>();
11 changes: 11 additions & 0 deletions packages/@ember/component/type-tests/template-only.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import templateOnlyComponent from '@ember/component/template-only';
import { TemplateOnlyComponentDefinition } from '@glimmer/runtime/dist/types/lib/component/template-only';
import { expectTypeOf } from 'expect-type';

expectTypeOf(templateOnlyComponent()).toEqualTypeOf<TemplateOnlyComponentDefinition>();

templateOnlyComponent('myModule');
templateOnlyComponent('myModule', 'myName');

// @ts-expect-error invalid params
templateOnlyComponent(1);
8 changes: 7 additions & 1 deletion packages/@ember/helper/type-tests/invoke-helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Component from '@ember/-internals/glimmer/lib/component';
import { getValue } from '@ember/-internals/metal';
import { Owner } from '@ember/-internals/owner';
import Helper from '@ember/component/helper';
import { invokeHelper } from '@ember/helper';
import { Cache } from '@glimmer/validator';
Expand All @@ -14,6 +15,9 @@ class PlusOne extends Helper {
}

export default class PlusOneComponent extends Component {
// Glint would help with this
declare args: { number: number };

plusOne = invokeHelper(this, PlusOne, () => {
return {
positional: [this.args.number],
Expand All @@ -25,6 +29,8 @@ export default class PlusOneComponent extends Component {
}
}

let component = new PlusOneComponent();
// Good enough for tests!
let owner = {} as Owner;
let component = new PlusOneComponent(owner);

expectTypeOf(component.plusOne).toEqualTypeOf<Cache<unknown>>();

0 comments on commit 4b1ccc2

Please sign in to comment.