Skip to content

Instantly share code, notes, and snippets.

@gossi
Last active August 3, 2023 20:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gossi/bff05b3a2b9eb0257ead3b4e03e3eaf5 to your computer and use it in GitHub Desktop.
Save gossi/bff05b3a2b9eb0257ead3b4e03e3eaf5 to your computer and use it in GitHub Desktop.
`ember-element-helper` signature thoughts

Early Signature API design:

type Positional<T> = [name: T];
type Return<T> = EmberComponent<{
  Element: T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element;
  Blocks: { default: [] };
}>;

export interface ElementSignature<T extends string> {
  Args: {
    Positional: Positional<T>;
  };
  Return: Return<T> | undefined;
}

that's fairly enough to use the (element) helper directly. But second level usage is when component authors give the option to pass in an element that will actually influence the Element of that particular component. And even further, that may happen conditionally.

Such a component is <CommandElement> from ember-command. The signature (stripped) in it's plain use:

interface CommandSignature {
  Element: HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement;
  Args: {
    command: CommandAction;
  };
}

That is the element is either an <a> or a <button> when a @command is present. When no command is present it will fall back to a <span>. However, here is where a consumer can pass in an element to take precendence. So the Signature might change to:

interface CommandSignature<
  T extends string = 'span',
  E extends ElementSignature<T> = ElementSignature<T>
> {
  Element: HTMLButtonElement | HTMLAnchorElement | E['Return']['Element']; // this will not work, as `E['Return']` is the component, not the signature of it
  Args: {
    command: CommandAction;
    element?: E['Return'];
  };
}

I'd like to have an ElementFor<...> utility type, that will return the Element from the "thing" (component or modifier) that has an assigned signature, then this can be properly typed:

interface CommandSignature<
  T extends string = 'span',
  E extends ElementSignature<T> = ElementSignature<T>
> {
  Element: HTMLButtonElement | HTMLAnchorElement | ElementFor<E['Return']>; // now that works
  Args: {
    command: CommandAction;
    element?: E['Return'];
  };
}

and ElementFor<> is what I'd see "similar" to WithBoundArgs<>, which would mean copy-paste/re-create types from: https://github.com/typed-ember/glint/blob/main/packages/template/-private


An alternative idea is to expose a ElementFromString<> utility type

type ElementFromString<S> = S extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[S] : Element;

with that being exported, the CommandHelperSignature can be written like so:

import type { ElementSignature, ElementFromString } from 'ember-element-helper';

interface CommandElementSignature<
  T extends string = 'span',
  E extends ElementSignature<T> = ElementSignature<T>
> {
  Element: HTMLButtonElement | HTMLAnchorElement | ElementFromString<T>;
  Args: {
    element?: E['Return'];
  };
}

At best, there is not even the need to export the signature, but general purpose utility types:

import { element } from 'ember-element-helper';
import type { ReturnFrom, ElementFor } from '@glint/template';

interface CommandElementSignature<
  T extends string = 'span',
  E extends element<T> = element<T>
> {
  Element: HTMLButtonElement | HTMLAnchorElement | ElementFor<E>;
  Args: {
    element?: ReturnFrom<E>;
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment