Skip to content

Attributes fail on components whose conditional Element type is instantiated with an unresolved generic #14

Open

Description

@NullVoxPopuli

Same setup as #12. Filed via Claude Code on NullVoxPopuli's behalf.

Inside a generic component, passing one of its own type params into another component's generic that feeds a conditional Element type makes every attribute on the invocation fail:

import Component from '@glimmer/component';

interface ButtonSignature<AsDiv extends boolean | undefined = undefined> {
	Element: AsDiv extends true ? HTMLDivElement : HTMLButtonElement;
	Args: { asDiv?: AsDiv };
}
declare const Button: abstract new <AsDiv extends boolean | undefined = undefined>() => Component<ButtonSignature<AsDiv>>;

interface Sig<IsFake extends boolean> { Args: { fake?: IsFake } }

export default class Probe<IsFake extends boolean = false> extends Component<Sig<IsFake>> {
	get asDiv(): IsFake {
		// @ts-expect-error subclassing trick
		return false;
	}

	<template><Button @asDiv={{this.asDiv}} id="x" /></template>
}
  • ember-tsc --noEmit: clean.
  • tsc --runExternalCode: error TS2345: Argument of type '{ id: "x"; }' is not assignable to parameter of type 'Partial<((IsFake extends true ? HTMLDivElement : HTMLButtonElement) extends null ? unknown : ...) extends HTMLElement ? ... : ...>'.

(Real-world shape: luna's Button with AsDiv fed by a Select component's own IsFake type param; every attribute on the invocation errored.) Looks like TS7 declines to evaluate the deferred conditional the way TS6 does. Workaround we shipped: widen the getter to boolean so the conditional resolves.

Activity

  1. NullVoxPopuli-ai-agent commented on Aug 27, 2026

    @NullVoxPopuli-ai-agent
    Contributor

    This is not a TypeScript 7 difference. TypeScript 5.9 on the transformed text reports the same error. ember-tsc hides it: the diagnostic anchors on generated text that no Volar verification mapping covers, and Volar drops diagnostics it cannot map back. Glint's template-to-typescript.ts describes this gap in its wideVerification comments. The mapper maps every synthesized region back to the template construct that produced it, so the error shows.

    The types that reject the code live in @glint/template:

    #24 records each in test/diagnostics-tests/upstream-differences/ so a change on either side shows up. The mapper keeps reporting them: dropping diagnostics on synthesized text would also hide real errors such as a missing required argument. Next step is a Glint report, after a check against the current Glint release.

  2. NullVoxPopuli commented on Aug 27, 2026

    @NullVoxPopuli
    OwnerAuthor

    Note

    This investigation was done with Claude (via Claude Code) — apologies in advance if it missed something; corrections welcome.

    Like #13, this reproduces standalone under both TS 6.0.3 and TS 7.1 nightly when the transformed output is checked directly (with ember-source/types + @glint/ember-tsc/types referenced) — so it's not a mapper or TS 7 divergence, it's inherent to how glint's attribute checking interacts with generic-dependent Element types.

    Mechanism

    Luna's Button declares (property-level generics, both defaulted):

    interface LunaButtonSignature<Href extends string | undefined = undefined, AsDiv extends boolean | undefined = undefined> {
      Element: AsDiv extends true ? HTMLDivElement : Href extends string ? HTMLAnchorElement : HTMLButtonElement;
      // ...
    }

    When a generic wrapper component threads its own unresolved type parameter into @href/@shouldRenderAsDiv:

    export default class Outer<Href extends string | undefined = undefined> extends Component<OuterSignature<Href>> {
      <template>
        <Button @href={{@href}} data-x="1">hi</Button>
      </template>
    }

    Href stays unresolved inside Outer's body, so Button's Element is a deferred conditional type. The transform emits:

    __glintDSL__.applyAttributes(__glintY__.element, { "data-x": "1" });

    and applyAttributes<T extends Element>(element: T, attrs: Partial<AttributesForElement<T>>) — where AttributesForElement<T> is itself a chain of conditionals over T — can never evaluate, so every attribute (even universally-valid data-*) fails:

    error TS2353: Object literal may only specify known properties, and '"data-x"' does not exist in type
    'Partial<((Href extends string ? HTMLAnchorElement : HTMLButtonElement) extends null ? unknown : ...) extends HTMLElement ? ... : ...>'
    

    Identical output from both compilers. Defaults on the generics don't help — the deferral comes from the outer component's unresolved parameter, not from Button's own parameters lacking defaults.

    Options

    1. Consumer-side (recommended, what we'll do in optro-frontend): generic wrappers shouldn't thread unresolved type params into Element-selecting args. Passing a concrete string | undefined makes the conditional evaluate (to HTMLButtonElement — slightly imprecise when an href is present, but checkable).
    2. @glint/template-side: there's no type-level way to detect "deferred conditional" and fall back, short of weakening applyAttributes for everyone (e.g. attrs: Partial<AttributesForElement<T>> | DataAttributes). I don't think that trade is worth it, but noting it for completeness.

    Happy to close this as by-design once the guidance is written down somewhere (docs / known-issues).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions