Here's an interesting limitation of Glint's handling of arguments we ran into last week. I'm leaving this here so we can hopefully (eventually!) chase it down. A colleague was attempting to accurately capture the semantics of a component which renders different tags dynamically.[^element-helper] He wanted to capture that the component in question applies ...attributes to an HTMLListElement if it receives @tagName='li' and to an HTMLDivElement if it receives @tagName='div'or nothing, so we wrote a signature like this:
This itself seems to work fine, but if you have another component which also has this same union-typed signature—so it can be similarly configurable and pass down ...attributes, while adding its own behavior; a fairly standard pattern, in other words—, things go badly:
Argument of type '[{ [NamedArgs]: true; tagName: "li" | "div" | undefined; }]' is not assignable to parameter of type '[named: { tagName: "li"; } & NamedArgsMarker] | [named?: ({ tagName?: "div" | undefined; } & NamedArgsMarker) | undefined]'.
Type '[{ [NamedArgs]: true; tagName: "li" | "div" | undefined; }]' is not assignable to type '[named?: ({ tagName?: "div" | undefined; } & NamedArgsMarker) | undefined]'.
Type '{ [NamedArgs]: true; tagName: "li" | "div" | undefined; }' is not assignable to type '{ tagName?: "div" | undefined; } & NamedArgsMarker'.
Type '{ [NamedArgs]: true; tagName: "li" | "div" | undefined; }' is not assignable to type '{ tagName?: "div" | undefined; }'.
Types of property 'tagName' are incompatible.
Type '"li" | "div" | undefined' is not assignable to type '"div" | undefined'.
By contrast, if we do what users would expect to be equivalent in a TS context:
function dynamicTag(args: DynamicTagSignature['Args']) {
if (args.tagName === 'li') {
console.log('li');
} else {
console.log('div');
}
}
function caller(args: DynamicTagSignature['Args']) {
dynamicTag({ tagName: args.tagName });
}
This works as expected, including all the things we would expect with excess property checks (see this playground). I also confirmed (though have since misplaced the code) that this also holds when adding a NamedArgsMarker the same way Glint does.
Glint's output:
An initial hypothesis, based on deconstructing the Glint intermediate representation, was that the return type from the resolve() overload we are using here—
—ends up distributing over the union while the naïve version I wrote in TS does not. But this is not the case: you can write a “dumb” version of resolve() which just uses Parameters to make sure it keeps the distributivity, and it still works:
declare function resolve<A extends any[], R, F extends (...args: A) => R>(
item: F | null | undefined
): (...args: Parameters<F>) => R;
resolve(dynamicTag)({ tagName: 'li' })
function caller(args: DynamicTagSignature['Args']) {
resolve(dynamicTag)({ tagName: args.tagName });
}
My next best guess is that it is somehow about the interaction between that and getting the types from Γ.args instead of "directly", but honestly? 🤷🏻♂️
[^element-helper]: This was more or less a workaround for a bug in ember-element-helper, but as is our habit, we are preserving the semantics of existing code when converting to TypeScript rather than changing behavior and adding types simultaneously.
Here's an interesting limitation of Glint's handling of arguments we ran into last week. I'm leaving this here so we can hopefully (eventually!) chase it down. A colleague was attempting to accurately capture the semantics of a component which renders different tags dynamically.[^element-helper] He wanted to capture that the component in question applies
...attributes
to anHTMLListElement
if it receives@tagName='li'
and to anHTMLDivElement
if it receives@tagName='div'
or nothing, so we wrote a signature like this:This itself seems to work fine, but if you have another component which also has this same union-typed signature—so it can be similarly configurable and pass down
...attributes
, while adding its own behavior; a fairly standard pattern, in other words—, things go badly:This produces the failure:
By contrast, if we do what users would expect to be equivalent in a TS context:
This works as expected, including all the things we would expect with excess property checks (see this playground). I also confirmed (though have since misplaced the code) that this also holds when adding a
NamedArgsMarker
the same way Glint does.Glint's output:
An initial hypothesis, based on deconstructing the Glint intermediate representation, was that the return type from the
resolve()
overload we are using here——ends up distributing over the union while the naïve version I wrote in TS does not. But this is not the case: you can write a “dumb” version of
resolve()
which just usesParameters
to make sure it keeps the distributivity, and it still works:My next best guess is that it is somehow about the interaction between that and getting the types from
Γ.args
instead of "directly", but honestly? 🤷🏻♂️[^element-helper]: This was more or less a workaround for a bug in
ember-element-helper
, but as is our habit, we are preserving the semantics of existing code when converting to TypeScript rather than changing behavior and adding types simultaneously.