Skip to content
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

Public API support disparity with Glint and typed templates with custom managers -- currently no story for TS support (for now?) #822

Closed
NullVoxPopuli opened this issue May 28, 2022 · 5 comments

Comments

@NullVoxPopuli
Copy link
Sponsor Contributor

NullVoxPopuli commented May 28, 2022

Today, support for helpers, components, modifiers, etc is all hard-coded in Glint.

For folks wanting to use glint, and have their custom-manager implementations support TS, there is no path here.

I think we should figure out a path for managers to provide the types to Glint, based on the type of thing received in the template -- since
aside from raw-values, everything involves a manger -- components, helpers, modifiers, etc.

For example,
all of these use custom managers and have no way to utilize typed templates (would currently show unresolveable errors to consumers of these addons):

(as a disclaimer, I used github's search, and picked out a few things -- idk how used these things are -- but it looks like many are used by sizable companies)

Atm, @glimmer/component, specifically has ExpandSignature: https://github.com/typed-ember/glint/blob/0e31e49301776d552429892d5749ecbf88f68d00/packages/template/-private/index.d.ts

RFC here: #748

So (atm), I'm kinda thinking we need some sort of "ExpandSignature" type thing per syntax, rather than per implementation.
So, right now we have a ExpandSignature for the glimmer/component -- but what about all the other components? Or how does Expanding the signature of a helper or modifier work? we know what these are statically as they're all matched on prototype, so I can imagine "something" like:

interface HelperManagerLookup {
  [SomeHelperPrototype]: SignaturueForThisHelper,
  [EmberHelperProtoype]: EmberHelperSignature,
}

(and similar for modifiers, components, and maybe eventually services, too? idk)

Relevant discussion in Discord: https://discord.com/channels/480462759797063690/814921339219476571/980229057931276308

@NullVoxPopuli NullVoxPopuli changed the title Public API support disparity with Glint and typed templates with for custom managers -- currently no story for TS support (for now?) Public API support disparity with Glint and typed templates with custom managers -- currently no story for TS support (for now?) May 28, 2022
@NullVoxPopuli
Copy link
Sponsor Contributor Author

NullVoxPopuli commented May 29, 2022

Reserving this post for my hacks and tradeoffs as I explore how to use Glint with gts and managers.


While writing this, I had an idea -- what if library authors provided a way for certain constructs to tie in to Glint in strict mode?

so, for example, for Resources, I'd do something like this:

// ember-resources/glint.ts
import type { HelperLike } from "@glint/template";
// ArgsWrapper is the traditional { named: {}, positional: [] } (abbrv)
import type { Resource, type ArgsWrapper } from './core';

type PositionalArgsOf<T extends ArgsWrapper> = T['positional'];
type NamedArgsOf<T extends ArgsWrapper> = T['named'];

interface Resource<Args extends ArgsWrapper> {
  [InTemplate]: HelperLike<{ 
    Args: { 
      Positional: PositionalArgsOf<Args>;
      Named: NamedArgsOf<Args> 
    } 
  }>;
}

Then Glint could check for existence of the [InTemplate] symbol and not have to figure out how to statically implement manager-lookup logic.

Glint could do something like

ExpandSignature<S> = S extends HasInTemplate ? S[InTemplate] : S

TS playground here


Resources

resources are reactive utilities that can easily be used in both JS and templates, and are completely type safe (except in Glint, where the assumption for HelperLike exists).
resources are helpers that can be used in JS with little-to-no ceremony.
resources may or may not have their own internal state.

tl;dr:

what I expect to work
import { tracked } from '@glimmer/tracking';
import { Resource } from 'ember-resources/core';

import type { TemplateOnlyComponent as TOC } from '@ember/component/template-only';

class ShadowHost extends Resource {
  @tracked value: ShadowRoot | undefined;

  update = (nextValue: ShadowRoot) => (this.value = nextValue);
}

const attachShadow = ((element: Element, setShadow: ShadowRoot['update']) => {
  setShadow(element.attachShadow({ mode: 'open' }));
});

// index.html has the production-fingerprinted references to these links
// Ideally, we'd have some pre-processor scan everything for references to
// assets in public, but idk how to set that up
const getStyles = () => {
  return [...document.head.querySelectorAll('link')].map(link => link.href);;
}

type Shadowed = TOC<{ Blocks: { default: [] } }>

<template signature:Shadowed>
  {{#let (ShadowHost) as |shadow|}}
    <div data-shadow {{attachShadow shadow.update}}></div>

    {{#if shadow.value}}
      {{#in-element shadow.value}}
        {{#let (getStyles) as |styles|}}
          {{#each styles as |styleHref|}}
            <link rel="stylesheet" href={{styleHref}}>
          {{/each}}
        {{/let}}

        {{yield}}
      {{/in-element}}
    {{/if}}
  {{/let}}
</template>

Note that I'm perfectly aware I could use modifier from ember-modifier, but that's not the point of this 😅

what I have to do today
import { tracked } from '@glimmer/tracking';
import { Resource } from 'ember-resources/core';

import type { TemplateOnlyComponent as TOC } from '@ember/component/template-only';
import type { ModifierLike, HelperLike } from "@glint/template";


/**
 * Every custom-manager using object needs to have two types.
 * - one for templates / glint
 * - one for JS/TS
 *
 *
 * Because Glint doesn't have an integration with the managers,
 * this complexity is pushed into user space.
 *
 * See issue report:
 *   https://github.com/emberjs/rfcs/issues/822#issuecomment-1140541910
 */
export class ShadowHost extends Resource {
  @tracked value: ShadowRoot | undefined;

  update = (nextValue: ShadowRoot) => (this.value = nextValue);
}

/**
 * Glint does not tie into any of the managers.
 * See issue report:
 *   https://github.com/emberjs/rfcs/issues/822
 */
const state = ShadowHost as unknown as HelperLike<{ Args: {}; Return: ShadowHost }>;


type UpdateFn = ShadowHost['update'];

const attachShadow = ((element: Element, setShadow: UpdateFn) => {
  setShadow(element.attachShadow({ mode: 'open' }));
/**
 * Because Glint doesn't have an integration with the managers,
 * this complexity is pushed into user space.
 *
 * See issue report:
 *   https://github.com/emberjs/rfcs/issues/822
 */
}) as unknown as ModifierLike<{ Args: { Positional: [UpdateFn] }}> ;

// index.html has the production-fingerprinted references to these links
// Ideally, we'd have some pre-processor scan everything for references to
// assets in public, but idk how to set that up
const getStyles = () => {
  return [...document.head.querySelectorAll('link')].map(link => link.href);;
}

const Shadowed: TOC<{
  Blocks: { default: [] }
}> =
<template>
  {{#let (state) as |shadow|}}
    <div data-shadow {{attachShadow shadow.update}}></div>

    {{#if shadow.value}}
      {{#in-element shadow.value}}
        {{#let (getStyles) as |styles|}}
          {{#each styles as |styleHref|}}
            <link rel="stylesheet" href={{styleHref}}>
          {{/each}}
        {{/let}}

        {{yield}}
      {{/in-element}}
    {{/if}}
  {{/let}}
</template>

export default Shadowed;
related issues

typed-ember/glint#340
typed-ember/glint#339


Normally, for templates, you'd add an entry into this structure:

import State from 'wherevere';

declare module "@glint/environment-ember-loose/registry" {
  export default interface Registry {
    state: HelperLike<{ Args: {}, Return: State }>;
  }
}

However, this does not work in strict mode, because your usage code looks like this:

import state from 'limber/helpers/state';

import type { TemplateOnlyComponent as TOC } from '@ember/component/template-only';

const Shadowed: TOC<{
  Blocks: { default: [] }
}> =
<template>
  {{#let (state) as |shadow|}}
      .... 
  {{/let}}
</template>

the type of state is used instead of whatever is in the registry -- which makes sense!

To remedy this _for template usage only, and break usage in regular TS files, I have to do:

const State = (this part actually doesn't matter);

export default State as unknown as HelperLike<{ Args: {}, Return: State}>;

As a per-app convention, one could employ a brittle (because human-enforced) convention of exporting two different things like this:

export const State = (this part actually doesn't matter);
export default State as unknown as HelperLike<{ Args: {}, Return: State}>;

So js/ts users would import { State } ... and Glint would read the default export.

however since in strict-mode, we can have things defined anywhere, defined locally, etc, there is no way to have a locally defined Resource (or any utility implemented with their own managers) that allow usage in JS and TS -- you'd have to do this hack every time you wanted to use something in a template:

const MyThing = '...'
const MyThingButForTemplates = '...' as unknown as HelperLike

@dfreeman
Copy link
Contributor

dfreeman commented May 30, 2022

Ok there's a lot to cover here, so I'm going to take it in order.

For folks wanting to use glint, and have their custom-manager implementations support TS, there is no path here.

Glint was designed from the ground up to support this; the entire point of the @glint/environment-* packages is to provide bindings between Glint's notion of how template types work and arbitrary implementations of components/helpers/modifiers. Those packages aren't privileged in any way, though, and the core doesn't have special knowledge of them—that's how we were able to prototype glint-environment-ember-template-imports in a standalone library before incorporating it into the monorepo.

I think we should figure out a path for managers to provide the types to Glint,

Managers are kind of a red herring for providing types to Glint, as the TS type system has no simple way of modeling "a value that has had this function called on it". We explored approaches to making that work early on, but ultimately it also turns out the manager type isn't 100% useful to use for working out how something is going to behave in a template even if we have it.

all of these use custom managers and have no way to utilize typed templates

Have you checked Glint's issue tracker? We've had conversations about how to do this 🙂

Atm, @glimmer/component, specifically has ExpandSignature: typed-ember/glint@0e31e49/packages/template/-private/index.d.ts
RFC here: #748
So (atm), I'm kinda thinking we need some sort of "ExpandSignature" type thing per syntax, rather than per implementation.

I think you've misunderstood what ExpandSignature is. It doesn't actually have anything to do with making Glint work—it is just about, well, "expanding a signature" from its shorthand form to the full longhand one as specified in the RFC you linked. The reason there's no helper or modifier equivalent is that there is no shorthand for @ember/component/helper or ember-modifier signatures. Other base component/helper/modifier implementations can use any structure they like to capture the relevant type information, as long as the key pieces are there somewhere (which you can see in the example below).

what if library authors provided a way for certain constructs to tie in to Glint in strict mode?

It's not specific to strict mode, but you're slowly working your way toward reinventing how Glint already works 😉

I'm not super familiar with the shape of the resource base class, but you should be able to write something along these lines to make Glint aware of how Resource subclasses behave in templates:

interface Resource<T extends ArgsWrapper> extends InstanceType<
  HelperLike<{
    Args: { Named: T['named']; Positional: T['positional'] };
    Return: IDontKnowHowToGetTheValueTypeOfAResourceButItGoesHere
  }>
> {}

This is exactly how we integrate Ember's own base classes into Glint (for example, here's Helper).

@wagenet
Copy link
Member

wagenet commented Jul 23, 2022

It sounds like @dfreeman is saying that the issues @NullVoxPopuli identified already have solutions. Is this a correct understanding? Is there any path forward for this ticket?

@NullVoxPopuli
Copy link
Sponsor Contributor Author

Sort of, we have my specific issue solved by extending upstream / third-party types like here: https://github.com/NullVoxPopuli/ember-statechart-component/blob/main/ember-statechart-component/src/glint.ts#L39

I can't speak for the typed-ember folks, but if this is the path forward for all situations like this, I'm happy with closing this

@dfreeman
Copy link
Contributor

Extending upstream types is the name of the game—it's how pretty much all of Glint works 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants