Skip to content

Commit e4135ba

Browse files
authored
Merge pull request #10 from typed-ember/conditional-narrowing
2 parents 8cbc291 + 7a5356e commit e4135ba

25 files changed

+380
-326
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export default class MyComponent extends Component<{ message: string }> {
301301
public static template = (() => {
302302
let χ!: typeof import('@glint/template');
303303
return χ.template(function* (𝚪: import('@glint/template').ResolveContext<MyComponent>) {
304-
χ.invokeInline(χ.resolveOrReturn(𝚪.args.message));
304+
χ.invokeEmit(χ.resolveOrReturn(𝚪.args.message));
305305
});
306306
})();
307307
}
Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { helper, fn as fnDefinition } from '@glimmerx/helper';
2-
import { resolve, invokeInline } from '@glint/environment-glimmerx/types';
2+
import { resolve } from '@glint/environment-glimmerx/types';
33
import { expectTypeOf } from 'expect-type';
4-
import { NoNamedArgs, ReturnsValue } from '@glint/template/-private';
4+
import { NoNamedArgs } from '@glint/template/-private';
55

66
// Built-in helper: `fn`
77
{
@@ -13,29 +13,25 @@ import { NoNamedArgs, ReturnsValue } from '@glint/template/-private';
1313
// @ts-expect-error: invalid arg
1414
fn({}, (t: string) => t, 123);
1515

16-
expectTypeOf(invokeInline(fn({}, () => true))).toEqualTypeOf<() => boolean>();
17-
expectTypeOf(invokeInline(fn({}, (arg: string) => arg.length))).toEqualTypeOf<
18-
(arg: string) => number
19-
>();
20-
expectTypeOf(invokeInline(fn({}, (arg: string) => arg.length, 'hi'))).toEqualTypeOf<
21-
() => number
22-
>();
16+
expectTypeOf(fn({}, () => true)).toEqualTypeOf<() => boolean>();
17+
expectTypeOf(fn({}, (arg: string) => arg.length)).toEqualTypeOf<(arg: string) => number>();
18+
expectTypeOf(fn({}, (arg: string) => arg.length, 'hi')).toEqualTypeOf<() => number>();
2319

2420
let identity = <T>(x: T): T => x;
2521

2622
// Bound type parameters are reflected in the output
27-
expectTypeOf(invokeInline(fn({}, identity, 'hi'))).toEqualTypeOf<() => string>();
23+
expectTypeOf(fn({}, identity, 'hi')).toEqualTypeOf<() => string>();
2824

29-
// Unfortunately unbound type parameters degrade to `unknown`; this is a known limitation
30-
expectTypeOf(invokeInline(fn({}, identity))).toEqualTypeOf<(x: unknown) => unknown>();
25+
// Unbound type parameters survive to the output
26+
expectTypeOf(fn({}, identity)).toEqualTypeOf<<T>(x: T) => T>();
3127
}
3228

3329
// Custom helper: positional params
3430
{
3531
let definition = helper(<T, U>([a, b]: [T, U]) => a || b);
3632
let or = resolve(definition);
3733

38-
expectTypeOf(or).toEqualTypeOf<<T, U>(args: NoNamedArgs, t: T, u: U) => ReturnsValue<T | U>>();
34+
expectTypeOf(or).toEqualTypeOf<<T, U>(args: NoNamedArgs, t: T, u: U) => T | U>();
3935

4036
// @ts-expect-error: extra named arg
4137
or({ hello: true }, 'a', 'b');
@@ -46,9 +42,9 @@ import { NoNamedArgs, ReturnsValue } from '@glint/template/-private';
4642
// @ts-expect-error: extra positional arg
4743
or({}, 'a', 'b', 'c');
4844

49-
expectTypeOf(invokeInline(or({}, 'a', 'b'))).toEqualTypeOf<string>();
50-
expectTypeOf(invokeInline(or({}, 'a', true))).toEqualTypeOf<string | boolean>();
51-
expectTypeOf(invokeInline(or({}, false, true))).toEqualTypeOf<boolean>();
45+
expectTypeOf(or({}, 'a', 'b')).toEqualTypeOf<string>();
46+
expectTypeOf(or({}, 'a', true)).toEqualTypeOf<string | boolean>();
47+
expectTypeOf(or({}, false, true)).toEqualTypeOf<boolean>();
5248
}
5349

5450
// Custom helper: named params
@@ -59,9 +55,7 @@ import { NoNamedArgs, ReturnsValue } from '@glint/template/-private';
5955

6056
let repeat = resolve(definition);
6157

62-
expectTypeOf(repeat).toEqualTypeOf<
63-
(args: { word: string; count?: number }) => ReturnsValue<Array<string>>
64-
>();
58+
expectTypeOf(repeat).toEqualTypeOf<(args: { word: string; count?: number }) => Array<string>>();
6559

6660
// @ts-expect-error: extra positional arg
6761
repeat({ word: 'hi' }, 123);
@@ -72,6 +66,6 @@ import { NoNamedArgs, ReturnsValue } from '@glint/template/-private';
7266
// @ts-expect-error: extra named arg
7367
repeat({ word: 'hello', foo: true });
7468

75-
expectTypeOf(invokeInline(repeat({ word: 'hi' }))).toEqualTypeOf<Array<string>>();
76-
expectTypeOf(invokeInline(repeat({ word: 'hi', count: 3 }))).toEqualTypeOf<Array<string>>();
69+
expectTypeOf(repeat({ word: 'hi' })).toEqualTypeOf<Array<string>>();
70+
expectTypeOf(repeat({ word: 'hi', count: 3 })).toEqualTypeOf<Array<string>>();
7771
}

packages/environment-glimmerx/types/globals.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as VM from '@glint/template/-private/keywords';
22

33
interface Keywords {
4+
// the `component` keyword exists but is essentially unusable as GlimmerX
5+
// has no string-based resolution and can only resolve statically-invoked
6+
// values as components/helpers/etc
7+
// component: VM.ComponentKeyword;
48
debugger: VM.DebuggerKeyword;
59
each: VM.EachKeyword;
6-
'has-block': VM.HasBlockParamsKeyword;
10+
'has-block': VM.HasBlockKeyword;
711
'has-block-params': VM.HasBlockParamsKeyword;
812
// the `if` keyword is implemented directly in @glint/transform
913
'in-element': VM.InElementKeyword;

packages/environment-glimmerx/types/signatures.d.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { NoNamedArgs, CreatesModifier } from '@glint/template/-private';
2-
import { Invokable, ReturnsValue } from '@glint/template/-private';
32
import { ResolutionKey } from '@glint/template/-private';
43
import { TemplateContext, AcceptsBlocks } from '@glint/template/-private';
54
import GlimmerXComponent from '@glimmerx/component';
@@ -17,7 +16,7 @@ declare module '@glint/template/resolution-rules' {
1716

1817
export interface SignatureResolutions<InvokedValue> {
1918
[ResolveGlimmerXComponent]: InvokedValue extends Constructor<GlimmerXComponent<infer Args>>
20-
? InvokedValue extends { template: Invokable<infer Signature> }
19+
? InvokedValue extends { template: infer Signature }
2120
? Signature
2221
: (args: Args) => AcceptsBlocks<{ default?: [] }>
2322
: never;
@@ -41,36 +40,36 @@ declare module '@glimmerx/modifier' {
4140
declare module '@glimmerx/helper' {
4241
export function helper<Result, Named = NoNamedArgs, Positional extends unknown[] = []>(
4342
fn: (positional: Positional, named: Named) => Result
44-
): Invokable<(args: Named, ...positional: Positional) => ReturnsValue<Result>>;
43+
): (args: Named, ...positional: Positional) => Result;
4544

4645
export function fn<Ret, Args extends unknown[]>(
4746
args: NoNamedArgs,
4847
f: (...rest: Args) => Ret
49-
): ReturnsValue<(...rest: Args) => Ret>;
48+
): (...rest: Args) => Ret;
5049
export function fn<A, Ret, Args extends unknown[]>(
5150
args: NoNamedArgs,
5251
f: (a: A, ...rest: Args) => Ret,
5352
a: A
54-
): ReturnsValue<(...rest: Args) => Ret>;
53+
): (...rest: Args) => Ret;
5554
export function fn<A, B, Ret, Args extends unknown[]>(
5655
args: NoNamedArgs,
5756
f: (a: A, b: B, ...rest: Args) => Ret,
5857
a: A,
5958
b: B
60-
): ReturnsValue<(...rest: Args) => Ret>;
59+
): (...rest: Args) => Ret;
6160
export function fn<A, B, C, Ret, Args extends unknown[]>(
6261
args: NoNamedArgs,
6362
f: (a: A, b: B, c: C, ...rest: Args) => Ret,
6463
a: A,
6564
b: B,
6665
c: C
67-
): ReturnsValue<(...rest: Args) => Ret>;
66+
): (...rest: Args) => Ret;
6867
export function fn<A, B, C, D, Ret, Args extends unknown[]>(
6968
args: NoNamedArgs,
7069
f: (a: A, b: B, c: C, d: D, ...rest: Args) => Ret,
7170
a: A,
7271
b: B,
7372
c: C,
7473
d: D
75-
): ReturnsValue<(...rest: Args) => Ret>;
74+
): (...rest: Args) => Ret;
7675
}

packages/template/-private/index.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { Invokable } from './invoke';
2-
export { AcceptsBlocks, CreatesModifier, ReturnsValue, NoNamedArgs } from './signature';
1+
export { AcceptsBlocks, CreatesModifier, NoNamedArgs } from './signature';
32
export { TemplateContext } from './template';
43
export { ResolutionKey } from './resolution';

packages/template/-private/invoke.d.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
1-
import { Return, ReturnsValue, CreatesModifier, AcceptsBlocks, AnySignature } from './signature';
1+
import { CreatesModifier, AcceptsBlocks } from './signature';
22
import { YieldsFromBlock, BlockBodies } from './blocks';
33

44
/**
5-
* This wrapping type indicates a value that may be invoked in a template.
6-
*
7-
* At present it's just a no-op type that does nothing but validate that
8-
* the given type is a valid template signature, as it's otherwise not
9-
* possible to do anything with function types in TS without losing information
10-
* about any type parameters they may have.
11-
*/
12-
export type Invokable<T extends AnySignature> = T;
13-
14-
/**
15-
* Invokes the given value as an inline invocation. This corresponds to a
16-
* mustache statement or subexpression, i.e. one of:
5+
* Invokes the given value as an inline expression to be emitted to the DOM.
6+
* This corresponds to a mustache statement either at the top level or being
7+
* passed as an attribute or concatenated into a string:
178
*
189
* {{value foo=bar}}
19-
* <div data-attr={{value foo=bar}}></div>
20-
* <div data-attr={{concat (value foo=bar)}}
21-
*
22-
* This form of invocation is the only one in a template that allows for a
23-
* value to be returned.
10+
* <div data-x={{value foo=bar}}>
11+
* <div data-x="hello {{value foo=bar}}">
2412
*/
25-
export declare function invokeInline<T extends ReturnsValue<any>>(
26-
value: T
27-
): ReturnType<T>[typeof Return];
13+
export declare function invokeEmit<
14+
T extends AcceptsBlocks<{}> | string | number | boolean | null | void
15+
>(value: T): void;
2816

2917
/**
3018
* Invokes the given value as a modifier. This corresponds to a mustache

packages/template/-private/keywords/component.d.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { AnySignature, AnyBlocks, ReturnsValue, AcceptsBlocks } from '../signature';
2-
import { ResolveSignature } from '../resolution';
3-
import { Invokable } from '../invoke';
1+
import { AnyBlocks, AcceptsBlocks } from '../signature';
2+
import { ResolveSignature, Resolvable } from '../resolution';
43

5-
type SignatureFor<T> = T extends AnySignature ? T : ResolveSignature<T>;
4+
type SignatureFor<T> = T extends Resolvable<any> ? ResolveSignature<T> : T;
65

76
type ArgsFor<T> = SignatureFor<T> extends (args: infer Args) => unknown ? Args : {};
87

@@ -23,12 +22,8 @@ export default interface ComponentKeyword {
2322
<Component, GivenArgs extends keyof ArgsFor<Component>>(
2423
args: { [Arg in GivenArgs]: ArgsFor<Component>[Arg] },
2524
component: Component
26-
): ReturnsValue<
27-
Invokable<
28-
(
29-
args: Omit<ArgsFor<Component>, GivenArgs> & Partial<Pick<ArgsFor<Component>, GivenArgs>>,
30-
...positional: PositionalFor<Component>
31-
) => AcceptsBlocks<BlocksFor<Component>>
32-
>
33-
>;
25+
): (
26+
args: Omit<ArgsFor<Component>, GivenArgs> & Partial<Pick<ArgsFor<Component>, GivenArgs>>,
27+
...positional: PositionalFor<Component>
28+
) => AcceptsBlocks<BlocksFor<Component>>;
3429
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { NoNamedArgs, ReturnsValue } from '../signature';
1+
import { NoNamedArgs } from '../signature';
22

33
export default interface DebuggerKeyword {
4-
(args: NoNamedArgs): ReturnsValue<void>;
4+
(args: NoNamedArgs): void;
55
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { NoNamedArgs, ReturnsValue } from '../signature';
1+
import { NoNamedArgs } from '../signature';
22

33
export default interface HasBlockParamsKeyword {
4-
(args: NoNamedArgs, blockName?: string): ReturnsValue<boolean>;
4+
(args: NoNamedArgs, blockName?: string): boolean;
55
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { NoNamedArgs, ReturnsValue } from '../signature';
1+
import { NoNamedArgs } from '../signature';
22

33
export default interface HasBlockKeyword {
4-
(args: NoNamedArgs, blockName?: string): ReturnsValue<boolean>;
4+
(args: NoNamedArgs, blockName?: string): boolean;
55
}

packages/template/-private/resolution.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* _template signature_ and _template context_ for various values.
44
*/
55

6-
import { ReturnsValue, AnySignature, NoNamedArgs } from './signature';
6+
import { NoNamedArgs } from './signature';
77
import { ContextResolutions, SignatureResolutions } from '../resolution-rules';
88

99
export declare const ResolutionKey: unique symbol;
@@ -55,7 +55,7 @@ export declare function resolve<T extends AnyGuard>(item: T): any;
5555
export declare function resolve<T extends Resolvable<SignatureResolutionKeys>>(
5656
item: T
5757
): ResolveSignature<T>;
58-
export declare function resolve<T extends AnySignature>(item: T): T;
58+
export declare function resolve<T extends Function>(item: T): T;
5959

6060
/**
6161
* A mustache like `{{this.foo}}` might either return a plain value like a string
@@ -70,5 +70,5 @@ export declare function resolveOrReturn<T extends AnyGuard>(item: T): any;
7070
export declare function resolveOrReturn<T extends Resolvable<SignatureResolutionKeys>>(
7171
item: T
7272
): ResolveSignature<T>;
73-
export declare function resolveOrReturn<T extends AnySignature>(item: T): T;
74-
export declare function resolveOrReturn<T>(item: T): (args: NoNamedArgs) => ReturnsValue<T>;
73+
export declare function resolveOrReturn<T extends Function>(item: T): T;
74+
export declare function resolveOrReturn<T>(item: T): (args: NoNamedArgs) => T;

packages/template/-private/signature.d.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,13 @@
2424
*/
2525

2626
declare const Modifier: unique symbol;
27-
declare const Return: unique symbol;
2827
declare const Blocks: unique symbol;
2928

3029
/** The loosest shape of a "blocks hash" */
3130
export type AnyBlocks = Record<string, unknown[]>;
3231

33-
/** The loosest shape of a template signature */
34-
export type AnySignature = (...args: any) => (blocks: any) => any;
35-
36-
/** Denotes that the associated entity returns a value when invoked */
37-
export type ReturnsValue<T> = () => { [Return]: T };
38-
3932
/** Denotes that the associated entity should be invoked as a modifier */
40-
export type CreatesModifier = () => { [Modifier]: true };
33+
export type CreatesModifier = { [Modifier]: true };
4134

4235
// These shenanigans are necessary to get TS to report when named args
4336
// are passed to a signature that doesn't expect any, because `{}` is

packages/template/-private/template.d.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { BlockYield } from './blocks';
88
import { AcceptsBlocks } from './signature';
9-
import { Invokable } from './invoke';
109

1110
/**
1211
* Determines the type of `this` and any `@arg`s used in a template.
@@ -18,18 +17,16 @@ export type TemplateContext<This, Args> = { this: This; args: Args };
1817

1918
/**
2019
* Accepts a generator function declaring an expected template context,
21-
* and returns an appropriate `Invokable` type that accepts the required
20+
* and returns an appropriate invokable type that accepts the required
2221
* named args and a set of blocks as determined by any `BlockYield`s
2322
* included in the generators iterator type.
2423
*/
2524
export declare function template<This, Args, Yields extends BlockYield<string, unknown[]>>(
2625
f: (𝚪: TemplateContext<This, Args>) => IterableIterator<Yields>
27-
): Invokable<
28-
(
29-
args: Args
30-
) => AcceptsBlocks<
31-
{
32-
[K in Yields['to']]?: Extract<Yields, { to: K }>['values'];
33-
}
34-
>
26+
): (
27+
args: Args
28+
) => AcceptsBlocks<
29+
{
30+
[K in Yields['to']]?: Extract<Yields, { to: K }>['values'];
31+
}
3532
>;

packages/template/__tests__/custom-invokable.test.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import SumType from 'sums-up';
33
import { resolve, toBlock, invokeBlock } from '@glint/template';
44
import { AcceptsBlocks, NoNamedArgs } from '@glint/template/-private/signature';
55
import { BlockYield } from '@glint/template/-private/blocks';
6-
import { Invokable } from '@glint/template/-private/invoke';
76

87
///////////////////////////////////////////////////////////////////////////////
98
// This module exercises what's possible when declaring a signature for a
@@ -22,22 +21,20 @@ type SumVariants<T extends SumType<never>> = T extends SumType<infer V> ? V : ne
2221
// https://github.com/hojberg/sums-up
2322
// It doesn't (can't) do exhaustiveness checking, but it does plumb through
2423
// type parameters correctly
25-
declare const caseOf: Invokable<<T extends SumType<never>>(
24+
declare const caseOf: <T extends SumType<never>>(
2625
args: NoNamedArgs,
2726
value: T
2827
) => AcceptsBlocks<{
2928
default: [
30-
Invokable<
31-
<K extends keyof SumVariants<T>>(
32-
args: NoNamedArgs,
33-
key: K
34-
) => AcceptsBlocks<{
35-
default: SumVariants<T>[K];
36-
inverse?: [];
37-
}>
38-
>
29+
<K extends keyof SumVariants<T>>(
30+
args: NoNamedArgs,
31+
key: K
32+
) => AcceptsBlocks<{
33+
default: SumVariants<T>[K];
34+
inverse?: [];
35+
}>
3936
];
40-
}>>;
37+
}>;
4138

4239
/**
4340
* ```hbs
@@ -75,9 +72,7 @@ expectTypeOf(
7572
// you do get exhaustiveness checking with this approach (though it's
7673
// arguable whether that's necessarily a good thing in template-land)
7774

78-
declare const CaseOf: Invokable<<T extends SumType<any>>(args: {
79-
value: T;
80-
}) => AcceptsBlocks<SumVariants<T>>>;
75+
declare const CaseOf: <T extends SumType<any>>(args: { value: T }) => AcceptsBlocks<SumVariants<T>>;
8176

8277
/**
8378
* ```hbs

0 commit comments

Comments
 (0)