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>;
};
}