Glint Types

ComponentLike, HelperLike and ModifierLike

While we often work in terms of specific implementations of components, helpers and modifiers, when we're using e.g. MyComponent in a template, it doesn't matter whether MyComponent is a template-only component, or a subclass of @glimmer/component or is a completely different object with a custom component manager.

To account for this, the @glint/template package provides a set of more general types: the ComponentLike, HelperLike and ModifierLike types describe any value that is usable as the respective type of entity in a template.

For example, in Ember all of the following values are ComponentLike:

  • a subclass of @glimmer/component

  • a subclass of @ember/component

  • the return value of templateOnlyComponent() from @ember/component/template-only

  • a <template> expression in a .gts file

  • the result of a {{component ...}} expression in a template

These types each accept signatures in the same format that the base classes for components and helpers/modifiers do.

WithBoundArgs and WithBoundPositionals

When you yield a "contextual component" (or helper or modifier), you need some way to declare the type of that value in the signature of the yielding component.

{{yield (hash banner=(component "some-banner" kind="warning"))}}

The return value from {{component}} component isn't the actual SomeBanner class—it won't have e.g. any of SomeBanner's static members, and it also no longer requires a @kind arg, since a default value has been set as part of the (component) invocation.

We could use ComponentLike to describe the type of this value:

import { ComponentLike } from '@glint/template';
import { SomeBannerSignature } from './some-banner';

interface MyComponentSignature {
  Blocks: {
    default: [{
      banner: ComponentLike<{
        Element: SomeBannerSignature['Element'];
        Blocks: SomeBannerSignature['Blocks'];
        Args: 
          Omit<SomeBannerSignature['Args'], 'kind'> 
            & { kind?: SomeBannerSignature['Args']['kind'] };
      }>;
    }];
  };
}

However, that's quite a lot of boilerplate to essentially express "it's like SomeBanner except kind is already set". Instead, you can use the WithBoundArgs type to express the same thing:

import { WithBoundArgs } from '@glint/template';
import SomeBanner from './some-banner';

interface MyComponentSignature {
  Blocks: {
    default: [{
      banner: WithBoundArgs<typeof SomeBanner, 'kind'>;
    }];
  };
}

If you had pre-bound multiple named args, you could union them together with the | type operator, e.g. 'kind' | 'title'.

Similarly, when working with a component/helper/modifier where you're pre-binding positional arguments, you can use WithBoundPositionals to indicate to downstream consumers that those arguments are already set:

{{yield (hash greetChris=(helper greetHelper "Chris"))}}
interface MyComponentSignature {
  Blocks: {
    default: [{
      greetChris: WithBoundPositionals<typeof greetHelper, 1>
    }];
  };
}

Where WithBoundArgs accepts the names of the pre-bound arguments, WithBoundPositionals accepts the number of positional arguments that are pre-bound, since binding a positional argument with {{component}}/{{modifier}}/{{helper}} sets that argument in a way that downstream users can't override.

Advanced Types Usage

From Glint's perspective, what makes a value usable as a component is being typed as a constructor for a value type that matches the instance type of ComponentLike. The same is true of helpers with HelperLike and modifiers with ModifierLike.

While this may seem like a negligible detail, making use of this fact can allow authors with a good handle on TypeScript's type system to pull off some very flexible "tricks" when working with Glint.

Custom Glint Entities

Ember (and the underlying Glimmer VM) has a notion of managers that allow authors to define custom values that act as components, helpers or modifiers when used in a template. Glint can't know how these custom entities will work, but by using ComponentLike/HelperLike/ModifierLike, you can explain to the typechecker how they function in a template.

For example, if you had a custom DOM-less "fetcher component" base class, you could use TypeScript declaration merging to tell Glint that its instance type extended InstanceType<ComponentLike<S>>, where S is an appropriate component signature based on how your custom component works.

// Define the custom component base class
class FetcherComponent<Params, Payload> {
  // ...
}

// Set its manager and, if necessary, template
setComponentManager(/*...*/, FetcherComponent);
setComponentTemplate(/*...*/, FetcherComponent);

// Use declaration merging to declare that the base class acts, from Glint's perspective,
// like a component with the given signature when used in a template.
interface FetcherComponent<Params, Payload> extends InstanceType<
  ComponentLike<{
    Args: { params: Params };
    Blocks: {
      loading: [];
      error: [message: string];
      ready: [payload: Payload];
    };
  }
>> {}

This is a fairly contrived example, and in most circumstances it would be simpler to use a standard base class like @glimmer/component, but nevertheless the option exists.

Note: this declaration merging technique using InstanceType<ComponentLike<...>> is exactly how Glint's own 1st-party environment packages like @glint/environment-ember-loose set up the template-aware types for @glimmer/component, @ember/component/helper, etc.

Type Parameters

When defining a class-based component, modifier or helper, you have a natural place to introduce any type parameters you may need. For example:

export interface MyEachSignature<T> {
  Args: { items: Array<T> };
  Blocks: {
    default: [item: T, index: number];
  };
}

export class MyEach<T> extends Component<MyEachSignature<T>> {
  // ...
}

However, if you aren't working with a concrete base type and can only say that your value is, for instance, some kind of ComponentLike, then TypeScript no longer offers you a place to introduce a type parameter into scope:

// 💥 Syntax error
declare const MyEach<T>: ComponentLike<MyEachSignature<T>>;

// 💥 Cannot find name 'T'. ts(2304)
declare const MyEach: ComponentLike<MyEachSignature<T>>;

Since what matters is the instance type, however, it is possible to define MyEach using just ComponentLike and slightly more type machinery:

declare const MyEach: abstract new <T>() => InstanceType<
  ComponentLike<MyEachSignature<T>>
>;

This shouldn't be a tool you frequently find the need to reach for, but it can be useful on occasion when working with complex declarations.

Last updated