Screen_Shot_2014-05-30_at_1.37.33_PM

THE SALSIFY SMARTER ENGINEERING BLOG

Introducing: Glint, a typed-template solution for GlimmerX and Ember

Posted by James C. Davis

Find me on:

Mar 24, 2021 12:45:34 PM

glint Glint

TL;DR?

  1. Install the Glint VSCode extension or see the Vim/Neovim section below.
  2. Add @glint/core and @glint/environment-ember-loose to your Ember app or addon.
  3. Create a .glintrc.yml file in the root of your app or addon containing:
      environment: ember-loose
  4. (for now) import from @glint/environment-ember-loose/glimmer-component in place of @glimmer/component.
  5. Enjoy squashing the red squiggles in your templates!
    (but, remember, Glint is still very experimental so no warranties, etc. etc.)

At Salsify, we care a lot about developer experience as well as producing high quality software. For these reasons, we’ve chosen to use TypeScript with Ember.js, our front-end framework of choice. TypeScript has helped us reduce bugs, ship features faster, and has been a boon to developer confidence. Our internal developer surveys, however, have shown one of the chief complaints when using Ember is the lack of type checking in component and route templates. And many users of TypeScript in the broader Ember community have also raised this concern. This is a tricky problem to solve because .hbs files are not something TypeScript knows how to process. Fortunately, there does exist at least one solution for this, which is the great Unstable Ember Language Server (UELS) addon els-addon-typed-templates created by Alex Kanunnikov (@lifeart). This addon brings Ember template type checking to editors that support the Language Server Protocol (LSP) and is the best tool for the job at this point. We highly recommend you use it for template type checking today.

Salsifarians Dan Freeman and James Davis along with Chris Krycho of LinkedIn make up the Typed Ember team, which has, for a number of years, maintained all things TypeScript in the Ember world including ember-cli-typescript, DefinitelyTyped definitions, and providing support and advice via the Ember Community Discord and other channels. At EmberConf 2019, James gave a talk on using TypeScript with Ember where he called out the “dirty little secret” that there is no type checking across templates. While the Ember TypeScript story has become quite good around things like services, utilities, and ember-data models, and even within components, templates are the glue that binds components (and thus our apps) together. The lack of ability to check types across these boundaries leaves a large plot hole. We end up with many component classes that are type-safe internally and in their direct calls out to other things written in TypeScript, but without any guarantees for anything invoked in the template, including helpers, modifiers, and other components. Indeed, there is not even checking of a component’s own properties in its template. Static type checking becomes exponentially more effective when it’s operating on a large contiguous unit rather than individual pieces.

To remedy this, the Typed Ember team has cooked up a solution we've named Glint. Architected by Dan Freeman, Glint consists of several pieces for type checking Ember and Glimmer templates, including a CLI for checking your whole project (useful for continuous integration) and full editor support. Like UELS, editor support is provided via a language server, so it should eventually work with any editor that supports the Language Service Protocol (LSP). We are initially targeting Visual Studio Code as this is by far the most popular editor in the Ember Community (based on the 2020 Ember Community Survey). We’ve also gotten it working with Vim/Neovim via Conquer of Completion (CoC) and describe how to set that up below. Additionally, similar to TypeScript itself, Glint's template type checker can be accessed programmatically and will eventually be integrated into the Ember build process similar to how ember-cli-typescript works today.

But Glint doesn't just check templates! Glint also provides feedback for .ts files based on their related templates. A good example of when this is important is private component properties. If you mark a component property you’re accessing only in the template as private, TypeScript will complain that it’s not used within the component class. Glint, however, is aware of component property usage in the template and won’t nag you about unused properties that are, in fact, used. To prevent VSCode’s builtin editor support (which is via tsserver) from providing conflicting feedback, we recommend disabling VSCode’s native TypeScript support for workspaces using Glint:

screenshot showing how to disable V S Code builtin TypeScript support

Template Type Checking in Action

Ok, this all sounds great, you say, but does Glint really bring the full power of TypeScript to your templates? Yes, it really does! Let’s take a look at some of the things you can do.

First of all, it checks types. 🙂

screenshot showing a type error on a component property in a template due to incorrect capitalization

Oh, wow, that would have been embarrassing.

But Glint provides editor support via a language server so it can do so much more! Forgot the type of that component property you’re using in the template? We’ve got you covered with quick info on hover:

screenshot showing component property type on hover

Hmm, something’s not quite right in this component I’m invoking here, can I quickly jump to the definition?

screenshot showing jumping to the definition of a component from where it is invoked in the template

Why yes, yes I can!

Ooh, you know what would be cool?: if I could rename a symbol in my component’s .ts file and the template would just update automatically (or vice versa).

animated gif showing renaming a property in the template and the property name updating in the component class

OMG! It works!

Wouldn’t it be nice to quickly discover everywhere a component is invoked (you know, right from the component)?

screenshot showing all invocations of a component

Got it!

Ok, these are great for grokking code and renaming things, but what about actually writing template code? Just give me my auto-complete already!!

screenshot showing auto-complete of component arguments in the component's template

Well, of course! There is a big caveat here, though. Glint uses @glimmer/syntax to parse templates and it doesn’t take kindly to invalid template syntax. This means that, say, if you haven’t closed your component invocation yet, you’re not going to get autocomplete. 😢 There are some tricks to get around the brittleness of @glimmer/syntax, but the real fix is to make @glimmer/syntax more fault-tolerant, which is a goal of the maintainers. In the meantime, we’re looking at ways to improve this experience.

Hopefully this little taste has whetted your appetite and you can’t wait to try out Glint. Let’s take a look at how to get it set up.

Getting Started

There’s just a few steps to get to template type checking goodness. The first (assuming you’re using Visual Studio Code) is to install the Glint VSCode extension. This tells VSCode where to find Glint’s language server, how to run it, and how to communicate with it. There is no configuration necessary, just install it and you should be good to go! Just remember to also disable VSCode’s builtin TypeScript support in your workspace as shown above so you don’t get conflicting feedback. The second step is to add the @glint/core package and either @glint/environment-ember-loose for standard Ember or @glint/environment-glimmerx for a GlimmerX app. This post is going to focus on Ember, but you can find the details for GlimmerX in the Glint Readme.

Next, to tell Glint what environment to use, drop a .glintrc.yml config file in the root of your project containing just:

environment: ember-loose

And we’re almost there. To use Glint today, you’ll need to do something that may feel a little peculiar and modify your component imports. You’ll need to import `Component` from

@glint/environment-ember-loose/glimmer-component

instead of

@glimmer/component

This is because the current type signature for @glimmer/component doesn’t allow you to specify enough information to fully type the component (you can specify component argument types, but there’s currently no way to provide types for what block components yield to blocks or for the element where attributes and element modifiers will be applied via ...attributes. The good news is that we’ll be opening an RFC soon to update the type signature of @glimmer/component to allow specifying these items (and potentially more as the need arises). But, in the meantime, @glint/environment-ember-loose/glimmer-component is functionally the same as @glimmer/component, just with a different type signature (more on this below).

At this point, things should work! Go open a .hbs component template that has a .ts backing class and you should get type checking! You’ll likely see lots of red squiggles, even for things that should be correct, but we’ll describe how to fix that below.

Again, here are the steps:

  1. Install the Glint VSCode extension.

  2. Add @glint/core and @glint/environment-ember-loose to your Ember app or addon.

  3. Create a .glintrc.yml file in the root of your app or addon containing:

    environment: ember-loose

  4. (for now) import from

    @glint/environment-ember-loose/glimmer-component

    in place of

    @glimmer/component

If this doesn’t get things going for you, feel free to reach out in #st-editor-support on the Ember Community Discord. If you think you’ve hit a bug, please file an issue in the Glint repo.

Vim/Neovim

For Vim and Neovim users, you can use Conquer of Completion (CoC) as a language server client and configure it to use Glint for .hbs and .ts files. We recommend doing this in a local workspace configuration so that CoC won’t try to use Glint for non-Glint projects.

In your app’s root directory start Vim/Neovim and run :CocLocalConfig.

If you don’t already have a local CoC config for your project, you’ll be asked to confirm you want to create a .vim directory (you do).

It will open or create a .vim/coc-settings.json file. Add this config:

{ 
  "tsserver.enable": false,
  "languageserver": {
    "glint": {
      "command": "${workspaceFolder}/node_modules/.bin/glint-language-server",
      "filetypes": ["html.handlebars","typescript"]
    }
  }
}

This will disable coc-tssever (if installed) and configure CoC to use Glint for .hbs and .ts files.

At some point we (or someone in the community) may consider creating a CoC extension for Glint, but for now, the above works just fine.

Other Editors

Because Glint editor support is implemented via a language server, it should be possible to make it work with any editor that has an LSP client. According to the 2020 Ember Community Survey, JetBrains IDEs are the second most popular editor (slightly more than Vim/Neovim). None of the Typed Ember team are JetBrains users, and we haven’t tried getting Glint running with any JetBrains IDE yet, but invite JetBrains users (and users of other editors that support LSP) to give it a try!

Glint CLI

With all this exciting stuff we can do in the editor, let’s not forget about the CLI! The Glint CLI is probably most useful as an additional CI check to make sure templates that don’t type check don’t get merged. But, like tsc, it’s also a good way to check your whole project or watch for breakage across your app when refactoring.

screenshot showing Glint CLI output indicating a type error in a template

To type check our whole app as you make changes, you can pass the --watch flag, similar to tsc, and Glint will pick up any changes file as you save them to disk and let you know if they break types.

Using Glint Effectively

Now that we’re checking types in templates, there’s a little bit of boilerplate we need to add to template invokables to use Glint effectively and get proper type checking.

Glimmer Components

As we alluded to earlier, we need to provide more type information about components. Glint expects a component type signature that includes types for arguments, yields (if it’s a block component), and the element(s) that ...attributes are applied to. The signature for a table component might look like:

export interface SuperTableSignature<T> {
  // We have a `<table>` as our root element
  Element: HTMLTableElement;
  // We accept an array of items, one per row
  Args: {
    items: Array<T>;
  };
  // We accept two named blocks: an optional `header`, and a required
  // `row`, which will be invoked with each item and its index.
  Yields: {
    header?: [];
    row: [item: T, index: number];
  };
}

Which is then applied to the component class like so:

export default class SuperTable<T> extends Component<SuperTableSignature<T>> {}

This will currently only type things correctly using the special Glint version of @glimmer/component:

import Component from '@glint/environment-ember-loose/glimmer-component';

All together this would look like:

import Component from '@glint/environment-ember-loose/glimmer-component';

export interface SuperTableSignature<T> {
  // We have a `<table>` as our root element
  Element: HTMLTableElement;
  // We accept an array of items, one per row
  Args: {
    items: Array<T>;
  };
  // We accept two named blocks: an optional `header`, and a required
  // `row`, which will be invoked with each item and its index.
  Yields: {
    header?: [];
    row: [item: T, index: number];
  };
}

export default class SuperTable<T> extends Component<SuperTableSignature<T>> {}

The template for this component might look like:

{{! app/components/super-table.hbs }}

<table ...attributes>
  {{#if (has-block "header")}}
    <thead>
      <tr>{{yield to="header"}}</tr>
    </thead>
  {{/if}}

  <tbody>
    {{#each @items as |item index|}}
      <tr>{{yield item index to="row"}}</tr>
    {{/each}}
  </tbody>
</table>

You’ll notice that the component example above should really have been a template-only component because the component class doesn’t actually do anything. Glint doesn’t quite support template-only components yet so you’ll need to define an empty component class for now.

Ember Components

If your app is not fully Octane, you can provide type information for Ember Components as well. Ember components also need a custom import. Since they don’t have this.args, they also need to use a special ArgsFor import and a bit more boilerplate:

// app/components/greeting.ts

import Component, {
ArgsFor
} from '@glint/environment-ember-loose/ember-component'; import { computed } from '@ember/object'; export interface GreetingSignature { Args: { message: string; target?: string; }; Yields: { default: [greeting: string]; }; } // This line declares that our component's args will be 'splatted'
// on to the instance:
export default interface Greeting extends ArgsFor<GreetingSignature> {} export default class Greeting extends Component<GreetingSignature> { @computed('target') private get greetingTarget() { // Therefore making `this.target` a legal `string | undefined`
// property access:
return this.target ?? 'World'; } }
{{! app/components/greeting.hbs }}

{{yield (concat @message ", " this.greetingTarget "!")}}

Template Registry

Today, Ember dynamically resolves things invoked in templates at runtime (though this will eventually change). Because of this dynamicity, we have to help Glint (and TypeScript) out a bit. We need a way to tell Glint where these things we’re invoking in templates are actually defined. We do this via a “type registry”. If you’ve used Ember Data with TypeScript, you’re probably familiar with adding models to a type registry at the bottom of each model.ts file. The Glint template type registry works exactly the same. For example:

// app/components/greeting.ts

import Component from '@glint/environment-ember-loose/glimmer-component';

export default class Greeting extends Component {
  
}

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry {
    Greeting: typeof Greeting;
  }
}

Just adding this little bit tells Glint how to find your component. We are looking at ways to possibly auto-generate this registry or possibly provide a codemod that will add this to all your TypeScript invokables automatically. The els-addon-typed-templates UELS plugin uses an auto-discovery approach. We’re concerned about the performance implications of this approach in large applications, and, with template strict mode coming soon, we’ve chosen to only support a type registry approach at this time.

Helpers

So far, we’ve only focused on components, but helpers are another type of invokable that we can type check in templates. Functional helpers only require using a temporary custom Glint import and adding to the registry:

import { helper } from '@glint/environment-ember-loose/ember-component/helper';

function repeat(params: [string, number]) {
  return params[0].repeat(params[1]);
}

const repeatHelper = helper(repeat);

export default repeatHelper;

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry {
    'repeat': typeof repeatHelper;
  }
}
{{! app/components/some-component.hbs }}

{{repeat "hello" 3}}

Class-based helpers require the custom import, but also require providing a type signature like components:

import Helper from '@glint/environment-ember-loose/ember-component/helper';

export interface AffixHelperSignature {
  NamedArgs: { prefix?: string, suffix?: string };
  PositionalArgs: [string];
  Return: string;
}

class AffixHelper extends Helper<AffixHelperSignature> {
  // TypeScript requires you to provide types for overridden methods. This
  // means you'll have to provide method types here that match the signature.
  // The best way to ensure this is to reference them directly as shown. 
  compute(
    params: AffixHelperSignature['PositionalArgs'],
    { prefix, suffix }: AffixHelperSignature['NamedArgs']
  ): AffixHelperSignature['Return'] {
    return `${prefix}${params[0]}${suffix}`;
  }
}

export default AffixHelper;

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry {
    'affix': typeof AffixHelper;
  }
}
{{! app/components/some-component.hbs }}

{{affix "kind" prefix="un"}}

{{affix "kind" suffix="ness"}}

{{affix "trust" prefix="en" suffix="ed"}}

Modifiers

These days we now have a third kind of thing that’s invokable in templates and that’s modifiers. Like functional helpers, functional modifiers just need a custom Glint import and addition to the registry. Consider this {{on}} clone:

// app/modifiers/when.ts

import { modifier } from "@glint/environment-ember-loose/ember-modifier";

const whenModifier = modifier(
  (element: HTMLElement, [eventName, handler]: [string, () => unknown]) => {
    element.addEventListener(eventName, handler);

    return () => {
      element.removeEventListener(eventName, handler);
    };
  }
);

export default whenModifier;

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry {
    when: typeof whenModifier;
  }
}
{{! app/components/some-component.hbs }}

<button type="button" {{when "click" this.onClick}}>
  Click me!
</button>

Class-based modifiers require additional type signature boilerplate like components:

// app/modifiers/say.ts

import Modifier from '@glint/environment-ember-loose/ember-modifier';

export interface SayModifierSignature {
  NamedArgs: { multiplier?: number };
  PositionalArgs: [input: string];
  Element: HTMLDivElement;
}

class SayModifier extends Modifier<SayModifierSignature> {
  didReceiveArguments(): void {
    this.element.innerText = this.args.positional[0].repeat(
      this.args.named.multiplier ?? 1
    );
  }

  willDestroy(): void {
    this.element.innerText = '';
  }
}

export default SayModifier;

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry {
    say: typeof SayModifier;
  }
}
{{! app/components/some-component.hbs }}

<div {{say "hello" multiplier=2}}>
</div>

Looking Under the Hood

So how does Glint actually do this? TypeScript doesn't understand template syntax so we have to convert the template into something TypeScript does understand. Glint uses a transform that converts the template syntax into TypeScript and injects it into the component class. This all happens in memory inside Glint, so you never actually see the transformed template in your files. The augmented component class is programmatically fed to the TypeScript compiler using the TypeScript Language Service API, which is designed for on-demand processing and quick feedback. Glint receives the feedback from the TypeScript compiler and maps the position in the augmented component class back to the original position in the template. It’s able to do this because it tracks positions when transforming the template. When used in an editor, the mapped feedback is then provided to the Glint language server, which sends it to the editor’s LSP client for display. The Glint CLI works similarly, and uses the same transform module, but uses the TypeScript Compiler API to process all files and provide output suitable for display in the terminal.

Glint is made up of several packages:

  • @glint/config - used for locating and loading the Glint config file
  • @glint/core - the Glint cli and language server
  • @glint/template - type declarations shared between environments
  • @glint/transform - tools for rewriting Glimmer templates as a TypeScript approximation of their semantics

Plus two environment packages:

As well as a Visual Studio Code extension for the Glint language server:

We're planning a follow-up deep dive into Glint internals, so look for more details coming soon!

JavaScript Support

So, Glint works for projects using TypeScript, but what if you’re not? Well, we’re working on that! Glint is powered by TypeScript, but TypeScript has the ability to extract type information from JavaScript files via @ts-check and JSDoc. We’re very fortunate to have Erin Singer of LinkedIn joining us to work on Glint JavaScript support. Work on this is just beginning, so stay tuned!

What’s Next

Glint, as it exists today, is still very experimental. We urge you to try it out, but don’t recommend using it for production apps and addons at this time. Again, Alex Kanunnikov’s Unstable Ember Language Server (UELS) addon els-addon-typed-templates is the go-to Ember template type checker right now. As part of official TypeScript support coming to Ember, we plan to propose updating the Glimmer component type signature to support all type information necessary for typed templates (likely the signature we’ve described above). Once this lands, you won’t need to use custom Glint imports and using Glint for production projects becomes much more tenable. We’d also like to work with Alex Kanunnikov (@lifeart) to integrate Glint into the Ember Language Server. For now, enjoy trying out Glint template type checking and let us know what you think! The new #st-editor-support channel on the Ember Community Discord is a great place to discuss Glint. Hope to see you there!

Topics: GlimmerX, Ember.js, Glimmer, TypeScript, typed-templates, template, type checking, typechecking

comments powered by Disqus

Recent Posts