-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
Comments
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 Glint could do something like
Resourcesresources 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 tl;dr: what I expect to workimport { 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 what I have to do todayimport { 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 issuesNormally, 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 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 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 |
Ok there's a lot to cover here, so I'm going to take it in order.
Glint was designed from the ground up to support this; the entire point of the
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.
Have you checked Glint's issue tracker? We've had conversations about how to do this 🙂
I think you've misunderstood what
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 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 |
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? |
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 |
Extending upstream types is the name of the game—it's how pretty much all of Glint works 🙂 |
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):
using the component manager
using the helper manager
using the modifier manager
what do we do if we add more managers? (like a service manager)
(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 hasExpandSignature
: https://github.com/typed-ember/glint/blob/0e31e49301776d552429892d5749ecbf88f68d00/packages/template/-private/index.d.tsRFC 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:(and similar for modifiers, components, and maybe eventually services, too? idk)
Relevant discussion in Discord: https://discord.com/channels/480462759797063690/814921339219476571/980229057931276308
The text was updated successfully, but these errors were encountered: