Skip to content

Simplify resolution and support plain function helpers in GlimmerX #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 6, 2021
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
6 changes: 4 additions & 2 deletions packages/environment-ember-loose/types/intrinsics/action.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Invokable } from '@glint/template/-private/resolution';

export type ActionNamedArgs<T> = {
value?: keyof T;
};
Expand All @@ -8,7 +10,7 @@ export type ActionResult<T, Args extends ActionNamedArgs<T>> = undefined extends
? T[Args['value']]
: T;

export interface ActionKeyword {
export type ActionKeyword = Invokable<{
<Ret, Args extends ActionNamedArgs<Ret>, Params extends unknown[]>(
args: Args,
f: (...rest: Params) => Ret
Expand Down Expand Up @@ -42,4 +44,4 @@ export interface ActionKeyword {
(args: ActionNamedArgs<Record<string, unknown>>, action: string, ...rest: unknown[]): (
...rest: unknown[]
) => unknown;
}
}>;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AcceptsBlocks, NoNamedArgs } from '@glint/template/-private';
import { Invokable } from '@glint/template/-private/resolution';

export interface EachInKeyword {
export type EachInKeyword = Invokable<{
<T>(args: NoNamedArgs, object: T): AcceptsBlocks<{
default: [key: keyof T, value: T[keyof T]];
}>;
}
}>;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AcceptsBlocks, NoNamedArgs } from '@glint/template/-private';
import { Invokable } from '@glint/template/-private/resolution';

export interface LinkToKeyword {
export type LinkToKeyword = Invokable<{
(args: NoNamedArgs, route: string, ...params: unknown[]): AcceptsBlocks<{
default?: [];
}>;
}
}>;

export interface LinkToArgs {
route: string;
Expand All @@ -17,6 +18,6 @@ export interface LinkToArgs {
query?: Record<string, unknown>;
}

export interface LinkToComponent {
export type LinkToComponent = Invokable<{
(args: LinkToArgs): AcceptsBlocks<{ default: [] }>;
}
}>;
5 changes: 3 additions & 2 deletions packages/environment-ember-loose/types/intrinsics/log.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NoNamedArgs } from '@glint/template/-private';
import { Invokable } from '@glint/template/-private/resolution';

export interface LogKeyword {
export type LogKeyword = Invokable<{
(args: NoNamedArgs, ...params: unknown[]): void;
}
}>;
15 changes: 8 additions & 7 deletions packages/environment-ember-loose/types/signatures.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import '@ember/component/helper';
import { ModifierArgs } from 'ember-modifier';

import { NoYields, NoNamedArgs, CreatesModifier } from '@glint/template/-private';
import { ContextType, SignatureType } from '@glint/template/-private';
import { ContextType, Invoke } from '@glint/template/-private';
import { TemplateContext, AcceptsBlocks } from '@glint/template/-private';
import { Invokable } from '@glint/template/-private/resolution';

declare module '@glimmer/component' {
export default interface Component<Args, Yields = NoYields> {
[SignatureType]: (args: Args) => AcceptsBlocks<Yields>;
[Invoke]: (args: Args) => AcceptsBlocks<Yields>;
[ContextType]: TemplateContext<this, Args, Yields>;
}
}

declare module '@ember/component' {
export default interface Component<Args = NoNamedArgs, Yields = NoYields> {
[SignatureType]: (args: Args) => AcceptsBlocks<Yields>;
[Invoke]: (args: Args) => AcceptsBlocks<Yields>;
[ContextType]: TemplateContext<this, Args, Yields>;
}
}
Expand All @@ -28,17 +29,17 @@ declare module '@ember/component/helper' {
Return = unknown
> {
compute(params: Positional, hash: Named): Return;
[SignatureType]: (named: Named, ...positional: Positional) => Return;
[Invoke]: (named: Named, ...positional: Positional) => Return;
}

export function helper<Positional extends unknown[] = [], Named = NoNamedArgs, Return = unknown>(
fn: (params: Positional, hash: Named) => Return
): (named: Named, ...positional: Positional) => Return;
): new () => Invokable<(named: Named, ...positional: Positional) => Return>;
}

declare module 'ember-modifier' {
export default interface ClassBasedModifier<Args extends ModifierArgs = ModifierArgs> {
[SignatureType]: (args: Args['named'], ...positional: Args['positional']) => CreatesModifier;
[Invoke]: (args: Args['named'], ...positional: Args['positional']) => CreatesModifier;
}

export function modifier<
Expand All @@ -47,5 +48,5 @@ declare module 'ember-modifier' {
Named = NoNamedArgs
>(
fn: (element: El, positional: Positional, named: Named) => unknown
): (named: Named, ...positional: Positional) => CreatesModifier;
): new () => Invokable<(named: Named, ...positional: Positional) => CreatesModifier>;
}
14 changes: 7 additions & 7 deletions packages/environment-glimmerx/__tests__/globals.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expectTypeOf } from 'expect-type';
import DebuggerKeyword from '@glint/template/-private/keywords/debugger';
import EachKeyword from '@glint/template/-private/keywords/each';
import HasBlockKeyword from '@glint/template/-private/keywords/has-block';
import HasBlockParamsKeyword from '@glint/template/-private/keywords/has-block-params';
import InElementKeyword from '@glint/template/-private/keywords/in-element';
import LetKeyword from '@glint/template/-private/keywords/let';
import WithKeyword from '@glint/template/-private/keywords/with';
import { DebuggerKeyword } from '@glint/template/-private/keywords/debugger';
import { EachKeyword } from '@glint/template/-private/keywords/each';
import { HasBlockKeyword } from '@glint/template/-private/keywords/has-block';
import { HasBlockParamsKeyword } from '@glint/template/-private/keywords/has-block-params';
import { InElementKeyword } from '@glint/template/-private/keywords/in-element';
import { LetKeyword } from '@glint/template/-private/keywords/let';
import { WithKeyword } from '@glint/template/-private/keywords/with';

import { Globals } from '@glint/environment-glimmerx/types';

Expand Down
41 changes: 40 additions & 1 deletion packages/environment-glimmerx/__tests__/helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { helper, fn as fnDefinition } from '@glimmerx/helper';
import { resolve } from '@glint/environment-glimmerx/types';
import { helper, fn2 as fnDefinition } from '@glimmerx/helper';
import { expectTypeOf } from 'expect-type';
import { NoNamedArgs } from '@glint/template/-private';

Expand Down Expand Up @@ -69,3 +69,42 @@ import { NoNamedArgs } from '@glint/template/-private';
expectTypeOf(repeat({ word: 'hi' })).toEqualTypeOf<Array<string>>();
expectTypeOf(repeat({ word: 'hi', count: 3 })).toEqualTypeOf<Array<string>>();
}

// Custom helper: bare function
{
let definition = <T>(item: T, count?: number): Array<T> => {
return Array.from({ length: count ?? 2 }, () => item);
};

let repeat = resolve(definition);

expectTypeOf(repeat).toEqualTypeOf<<T>(args: NoNamedArgs, item: T, count?: number) => Array<T>>();

// @ts-expect-error: unexpected named arg
repeat({ word: 'hi' }, 123, 12);

// @ts-expect-error: missing required positional arg
repeat({});

// @ts-expect-error: extra positional arg
repeat({}, 'hi', 12, 'ok');

expectTypeOf(repeat({}, 123)).toEqualTypeOf<Array<number>>();
expectTypeOf(repeat({}, 'hi', 5)).toEqualTypeOf<Array<string>>();
}

// Custom helper: type guard
{
let definition = (arg: unknown): arg is string => typeof arg === 'string';

let isString = resolve(definition);

expectTypeOf(isString).toEqualTypeOf<(args: NoNamedArgs, arg: unknown) => arg is string>();

let x = 'hi' as string | number;
if (isString({}, x)) {
expectTypeOf(x).toEqualTypeOf<string>();
} else {
expectTypeOf(x).toEqualTypeOf<number>();
}
}
2 changes: 1 addition & 1 deletion packages/environment-glimmerx/__tests__/modifier.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { on as onDefinition } from '@glimmerx/modifier';
import { on2 as onDefinition } from '@glimmerx/modifier';
import { resolve, invokeModifier } from '@glint/environment-glimmerx/types';
import { expectTypeOf } from 'expect-type';

Expand Down
44 changes: 44 additions & 0 deletions packages/environment-glimmerx/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,47 @@ import './signatures';

export * from '@glint/template';
export { Globals } from './globals';

/*
* Since GlimmerX supports using bare functions as helpers that only
* accept positional parameters, we can't just use the core definitions
* of `resolve` and `resolveOrReturn`. Instead we need to export versions
* with additional overloads at the correct point in the chain to handle
* those functions correctly.
*
* In order, we have:
* - explicit `Invokable<T>`
* - constructor for an `Invokable<T>`
* - a plain type guard
* - any other kind of plain function
*
* And in the case of `resolveOrReturn`, a final fallback for any other
* type of value. See the upstream definitions in `@glint/template` for
* further details on resolution.
*/

import { Invokable, Invoke } from '@glint/template/-private/resolution';
import { NoNamedArgs } from '@glint/template/-private/signature';

export declare function resolve<T extends Invokable>(item: T): T[typeof Invoke];
export declare function resolve<Args extends unknown[], Instance extends Invokable>(
item: new (...args: Args) => Instance
): (...args: Parameters<Instance[typeof Invoke]>) => ReturnType<Instance[typeof Invoke]>;
export declare function resolve<Value, Args extends unknown[], T extends Value>(
item: (value: Value, ...args: Args) => value is T
): (named: NoNamedArgs, value: Value, ...args: Args) => value is T;
export declare function resolve<Args extends unknown[], T>(
item: (...args: Args) => T
): (named: NoNamedArgs, ...args: Args) => T;

export declare function resolveOrReturn<T extends Invokable>(item: T): T[typeof Invoke];
export declare function resolveOrReturn<Args extends unknown[], Instance extends Invokable>(
item: new (...args: Args) => Instance
): (...args: Parameters<Instance[typeof Invoke]>) => ReturnType<Instance[typeof Invoke]>;
export declare function resolveOrReturn<Value, Args extends unknown[], T extends Value>(
item: (value: Value, ...args: Args) => value is T
): (named: NoNamedArgs, value: Value, ...args: Args) => value is T;
export declare function resolveOrReturn<Args extends unknown[], T>(
item: (...args: Args) => T
): (named: NoNamedArgs, ...args: Args) => T;
export declare function resolveOrReturn<T>(item: T): (args: NoNamedArgs) => T;
76 changes: 41 additions & 35 deletions packages/environment-glimmerx/types/signatures.d.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,61 @@
import { NoNamedArgs, CreatesModifier, NoYields } from '@glint/template/-private';
import { ContextType, SignatureType } from '@glint/template/-private';
import { ContextType, Invoke } from '@glint/template/-private';
import { TemplateContext, AcceptsBlocks } from '@glint/template/-private';
import { Invokable } from '@glint/template/-private/resolution';

declare module '@glimmerx/component' {
export default interface Component<Args, Yields = NoYields> {
[SignatureType]: (args: Args) => AcceptsBlocks<Yields>;
[Invoke]: (args: Args) => AcceptsBlocks<Yields>;
[ContextType]: TemplateContext<this, Args, Yields>;
}
}

declare module '@glimmerx/modifier' {
export function on<Name extends keyof HTMLElementEventMap>(
// TODO: this is really bringing https://github.com/typed-ember/glint/issues/25 to a head
// We need to stop trying to augment existing types and instead setup re-exports with our
// own proper types.
export const on2: Invokable<<Name extends keyof HTMLElementEventMap>(
args: NoNamedArgs,
name: Name,
callback: (event: HTMLElementEventMap[Name]) => void
): CreatesModifier;
) => CreatesModifier>;
}

declare module '@glimmerx/helper' {
export function helper<Result, Named = NoNamedArgs, Positional extends unknown[] = []>(
fn: (positional: Positional, named: Named) => Result
): (args: Named, ...positional: Positional) => Result;
): new () => Invokable<(args: Named, ...positional: Positional) => Result>;

export function fn<Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (...rest: Args) => Ret
): (...rest: Args) => Ret;
export function fn<A, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, ...rest: Args) => Ret,
a: A
): (...rest: Args) => Ret;
export function fn<A, B, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, b: B, ...rest: Args) => Ret,
a: A,
b: B
): (...rest: Args) => Ret;
export function fn<A, B, C, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, b: B, c: C, ...rest: Args) => Ret,
a: A,
b: B,
c: C
): (...rest: Args) => Ret;
export function fn<A, B, C, D, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, b: B, c: C, d: D, ...rest: Args) => Ret,
a: A,
b: B,
c: C,
d: D
): (...rest: Args) => Ret;
// TODO: this is really bringing https://github.com/typed-ember/glint/issues/25 to a head
// We need to stop trying to augment existing types and instead setup re-exports with our
// own proper types.
export const fn2: Invokable<{
<Ret, Args extends unknown[]>(args: NoNamedArgs, f: (...rest: Args) => Ret): (
...rest: Args
) => Ret;
<A, Ret, Args extends unknown[]>(args: NoNamedArgs, f: (a: A, ...rest: Args) => Ret, a: A): (
...rest: Args
) => Ret;
<A, B, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, b: B, ...rest: Args) => Ret,
a: A,
b: B
): (...rest: Args) => Ret;
<A, B, C, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, b: B, c: C, ...rest: Args) => Ret,
a: A,
b: B,
c: C
): (...rest: Args) => Ret;
<A, B, C, D, Ret, Args extends unknown[]>(
args: NoNamedArgs,
f: (a: A, b: B, c: C, d: D, ...rest: Args) => Ret,
a: A,
b: B,
c: C,
d: D
): (...rest: Args) => Ret;
}>;
}
2 changes: 1 addition & 1 deletion packages/template/-private/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { AcceptsBlocks, CreatesModifier, NoNamedArgs, NoYields } from './signature';
export { SignatureType, ContextType } from './resolution';
export { Invoke, ContextType } from './resolution';
export { TemplateContext } from './template';
25 changes: 9 additions & 16 deletions packages/template/-private/keywords/component.d.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import { AcceptsBlocks, AnyBlocks } from '../signature';
import { HasSignature } from '../resolution';
import { Invokable } from '../resolution';

export default interface ComponentKeyword {
// Invoking with a component class
export type ComponentKeyword = Invokable<{
<
Args,
GivenArgs extends Partial<Args>,
Blocks extends AnyBlocks,
ConstructorArgs extends unknown[]
>(
args: GivenArgs,
component: new (...args: ConstructorArgs) => HasSignature<(args: Args) => AcceptsBlocks<Blocks>>
): (
args: Omit<Args, keyof GivenArgs> & Partial<Pick<Args, keyof GivenArgs & keyof Args>>
) => AcceptsBlocks<Blocks>;

// Invoking with the result of another `{{component}}` expression
<Args, GivenArgs extends Partial<Args>, Blocks extends AnyBlocks>(
args: GivenArgs,
component: (args: Args) => AcceptsBlocks<Blocks>
): (
args: Omit<Args, keyof GivenArgs> & Partial<Pick<Args, keyof GivenArgs & keyof Args>>
) => AcceptsBlocks<Blocks>;
}
component: new (...args: ConstructorArgs) => Invokable<(args: Args) => AcceptsBlocks<Blocks>>
): new () => Invokable<
(
args: Omit<Args, keyof GivenArgs> & Partial<Pick<Args, keyof GivenArgs & keyof Args>>
) => AcceptsBlocks<Blocks>
>;
}>;
5 changes: 3 additions & 2 deletions packages/template/-private/keywords/debugger.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Invokable } from '../resolution';
import { NoNamedArgs } from '../signature';

export default interface DebuggerKeyword {
export type DebuggerKeyword = Invokable<{
(args: NoNamedArgs): void;
}
}>;
5 changes: 3 additions & 2 deletions packages/template/-private/keywords/each.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Invokable } from '../resolution';
import { AcceptsBlocks } from '../signature';

export default interface EachKeyword {
export type EachKeyword = Invokable<{
<T>(args: { key?: string }, items: T[]): AcceptsBlocks<{
default: [T, number];
inverse?: [];
}>;
}
}>;
Loading