From aea4373470a9eef03a0e3de164f8481dfa965f03 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Tue, 30 Mar 2021 21:52:51 +0200 Subject: [PATCH 1/3] Class-based helpers may return `undefined` --- .../__tests__/helper.test.ts | 15 +++++++++++++++ .../ember-component/helper.ts | 6 ++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/environment-ember-loose/__tests__/helper.test.ts b/packages/environment-ember-loose/__tests__/helper.test.ts index e8ebd2e64..e785a3f2e 100644 --- a/packages/environment-ember-loose/__tests__/helper.test.ts +++ b/packages/environment-ember-loose/__tests__/helper.test.ts @@ -72,3 +72,18 @@ import { EmptyObject } from '@glint/template/-private/integration'; expectTypeOf(repeat({ value: 'hi' })).toEqualTypeOf>(); expectTypeOf(repeat({ value: 123, count: 3 })).toEqualTypeOf>(); } + +// Class-based helpers can return undefined +{ + class MaybeStringHelper extends Helper<{ Return: string | undefined }> { + compute(): string | undefined { + if (Math.random() > 0.5) { + return 'ok'; + } + } + } + + let maybeString = resolve(MaybeStringHelper); + + expectTypeOf(maybeString).toEqualTypeOf<(args: EmptyObject) => string | undefined>(); +} diff --git a/packages/environment-ember-loose/ember-component/helper.ts b/packages/environment-ember-loose/ember-component/helper.ts index 0b2dbc5c6..7e81a6844 100644 --- a/packages/environment-ember-loose/ember-component/helper.ts +++ b/packages/environment-ember-loose/ember-component/helper.ts @@ -8,9 +8,7 @@ type EmberHelperConstructor = typeof import('@ember/component/helper').default; const EmberHelper = Ember.Helper; const emberHelper = Ember.Helper.helper; -type Get = Key extends keyof T - ? Exclude - : Otherwise; +type Get = Key extends keyof T ? T[Key] : Otherwise; type HelperFactory = ( fn: (params: Positional, hash: Named) => Return @@ -39,7 +37,7 @@ interface Helper extends Omit [Invoke]: ( named: Get, - ...positional: Get + ...positional: Exclude, undefined> ) => Get; } From dc80383fda2f045a392ab49e05feaf6149fff372 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Wed, 31 Mar 2021 23:01:55 +0200 Subject: [PATCH 2/3] `{{each}}` has Ember-specific semantics in `environment-ember-loose` --- .../-private/dsl/globals.d.ts | 3 +- .../-private/intrinsics/each.d.ts | 11 ++++ .../__tests__/intrinsics/each.test.ts | 54 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/environment-ember-loose/-private/intrinsics/each.d.ts create mode 100644 packages/environment-ember-loose/__tests__/intrinsics/each.test.ts diff --git a/packages/environment-ember-loose/-private/dsl/globals.d.ts b/packages/environment-ember-loose/-private/dsl/globals.d.ts index 1bc6b504c..28a8e0dba 100644 --- a/packages/environment-ember-loose/-private/dsl/globals.d.ts +++ b/packages/environment-ember-loose/-private/dsl/globals.d.ts @@ -3,6 +3,7 @@ import * as VM from '@glint/template/-private/keywords'; import { ActionKeyword } from '../intrinsics/action'; import { ComponentKeyword } from '../intrinsics/component'; import { ConcatHelper } from '../intrinsics/concat'; +import { EachKeyword } from '../intrinsics/each'; import { EachInKeyword } from '../intrinsics/each-in'; import { FnHelper } from '../intrinsics/fn'; import { GetHelper } from '../intrinsics/get'; @@ -59,7 +60,7 @@ interface Keywords { [the API documentation]: https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/each?anchor=each */ - each: VM.EachKeyword; + each: EachKeyword; /** The `{{each-in}}` helper loops over properties on an object. diff --git a/packages/environment-ember-loose/-private/intrinsics/each.d.ts b/packages/environment-ember-loose/-private/intrinsics/each.d.ts new file mode 100644 index 000000000..ebfca1f49 --- /dev/null +++ b/packages/environment-ember-loose/-private/intrinsics/each.d.ts @@ -0,0 +1,11 @@ +import { AcceptsBlocks, DirectInvokable } from '@glint/template/-private/integration'; +import EmberArray from '@ember/array'; + +type ArrayLike = ReadonlyArray | Iterable | EmberArray; + +export type EachKeyword = DirectInvokable<{ + (args: { key?: string }, items: ArrayLike): AcceptsBlocks<{ + default: [T, number]; + inverse?: []; + }>; +}>; diff --git a/packages/environment-ember-loose/__tests__/intrinsics/each.test.ts b/packages/environment-ember-loose/__tests__/intrinsics/each.test.ts new file mode 100644 index 000000000..418475542 --- /dev/null +++ b/packages/environment-ember-loose/__tests__/intrinsics/each.test.ts @@ -0,0 +1,54 @@ +import { expectTypeOf } from 'expect-type'; +import { Globals, resolve, invokeBlock } from '@glint/environment-ember-loose/-private/dsl'; +import ArrayProxy from '@ember/array/proxy'; + +let each = resolve(Globals['each']); + +// Yield out array values and indices + +invokeBlock(each({}, ['a', 'b', 'c']), { + default(value, index) { + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(index).toEqualTypeOf(); + }, + inverse(...args) { + expectTypeOf(args).toEqualTypeOf<[]>(); + }, +}); + +// Works for Ember arrays + +declare const proxiedArray: ArrayProxy; + +invokeBlock(each({}, proxiedArray), { + default(value, index) { + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(index).toEqualTypeOf(); + }, +}); + +// Works for other iterables + +invokeBlock(each({}, new Map()), { + default([key, value], index) { + expectTypeOf(key).toEqualTypeOf(); + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(index).toEqualTypeOf(); + }, +}); + +// Works for `readonly` arrays + +invokeBlock(each({}, ['a', 'b', 'c'] as readonly string[]), { + default(value, index) { + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(index).toEqualTypeOf(); + }, +}); + +// Accept a `key` string +invokeBlock(each({ key: 'id' }, [{ id: 1 }]), { + default() { + // Don't yield + }, +}); From c7cc5c2ff77e0501513a8f2df7c3fa211823b394 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Wed, 31 Mar 2021 23:06:03 +0200 Subject: [PATCH 3/3] `{{get}}` handles `ObjectProxy` instances --- .../environment-ember-loose/-private/intrinsics/get.d.ts | 2 ++ .../__tests__/intrinsics/get.test.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/environment-ember-loose/-private/intrinsics/get.d.ts b/packages/environment-ember-loose/-private/intrinsics/get.d.ts index 61fbbfe9e..c92d794db 100644 --- a/packages/environment-ember-loose/-private/intrinsics/get.d.ts +++ b/packages/environment-ember-loose/-private/intrinsics/get.d.ts @@ -1,6 +1,8 @@ import { DirectInvokable, EmptyObject } from '@glint/template/-private/integration'; +import ObjectProxy from '@ember/object/proxy'; export type GetHelper = DirectInvokable<{ (args: EmptyObject, obj: T, key: K): T[K]; + (args: EmptyObject, obj: ObjectProxy, key: K): T[K]; (args: EmptyObject, obj: unknown, key: string): unknown; }>; diff --git a/packages/environment-ember-loose/__tests__/intrinsics/get.test.ts b/packages/environment-ember-loose/__tests__/intrinsics/get.test.ts index 8a592d815..1cee7a0e1 100644 --- a/packages/environment-ember-loose/__tests__/intrinsics/get.test.ts +++ b/packages/environment-ember-loose/__tests__/intrinsics/get.test.ts @@ -1,5 +1,6 @@ import { expectTypeOf } from 'expect-type'; import { Globals, resolve } from '@glint/environment-ember-loose/-private/dsl'; +import ObjectProxy from '@ember/object/proxy'; let get = resolve(Globals['get']); @@ -17,3 +18,10 @@ get( {}, 'hi' ); + +// Getting a value off an ObjectProxy +declare const proxiedObject: ObjectProxy<{ name: string }>; + +expectTypeOf(get({}, proxiedObject, 'content')).toEqualTypeOf<{ name: string }>(); +expectTypeOf(get({}, proxiedObject, 'name')).toEqualTypeOf(); +expectTypeOf(get({}, proxiedObject, 'unknownKey')).toEqualTypeOf();