Skip to content

fix(#1068): preserve generic T when curried component is passed to another generic component#1

Draft
johanrd wants to merge 1 commit into
mainfrom
fix/1068-deep-generic-propagation
Draft

fix(#1068): preserve generic T when curried component is passed to another generic component#1
johanrd wants to merge 1 commit into
mainfrom
fix/1068-deep-generic-propagation

Conversation

@johanrd

@johanrd johanrd commented Mar 16, 2026

Copy link
Copy Markdown
Owner

A possible direction for typed-ember#1068 (comment)

When {{#let (component Cell onSelect=@onselect) as |BoundCell|}} is used and BoundCell is passed to another generic component like <Row @BoundCell={{BoundCell}}>, TypeScript's higher-order inference preserves Cell's generic T as an independent type parameter, causing the consumer's T to default to the constraint instead of the enclosing scope's abstract T.

Fix: ScopeStack tracks bind-invokable block params from {{#let}} via ref-counted map. When these are used as args to other components, they're cast to any to prevent the independent generic from overriding T inference. Type safety is maintained by the keyword's validation in the comma expression's first stage.

Written by claude

…assed to another generic component

When {{#let (component Cell onSelect=@onselect) as |BoundCell|}} is
used and BoundCell is passed to another generic component like
<Row @BoundCell={{BoundCell}}>, TypeScript's higher-order inference
preserves Cell's generic T as an independent type parameter, causing
the consumer's T to default to the constraint instead of the enclosing
scope's abstract T.

Fix: ScopeStack tracks bind-invokable block params from {{#let}} via
ref-counted map. When these are used as args to other components,
they're cast to `any` to prevent the independent generic from
overriding T inference. Type safety is maintained by the keyword's
validation in the comma expression's first stage.
NullVoxPopuli-ai-agent pushed a commit to NullVoxPopuli-ai-agent/glint that referenced this pull request Jul 4, 2026
…consumer inference

A generic class component curried via {{component}} keeps its own type
parameter free in the emitted bindInvokable result -- deliberately, since
that is what lets the curried value satisfy generic-shaped targets like
WithBoundArgs<typeof C, K> (typically reached via {{yield}}). But when a
{{#let}}-bound alias of such a value was passed as an ARG to another generic
component, TypeScript instantiated the free generic to its constraint during
inference, and that collapsed candidate beat (or conflicted with) the correct
candidates from sibling args: in the issue's DataTable -> Row -> Cell chain,
Row's T became Identifiable instead of the enclosing T.

No library-signature shape can express "pin the curried generic from the
bound args" -- TypeScript only instantiates a generic source signature in
context when the target signature is otherwise fully concrete (several
attempted shapes are documented in the PR). Instead, the transform now
tracks {{#let}} block params initialized from bind-invokable subexpressions
with named args (shadow-correct, via ScopeStack binding metadata) and casts
exactly those references, in arg position only, to the new
InferenceInertInvokable type: the value contributes no inference candidates
while remaining an Invokable, and every other position (notably {{yield}})
keeps the fully-typed reference.

Trade-off: at those arg positions, compatibility between the curried
component's signature and the consuming arg's declared type is not checked.

Based on the approach explored in johanrd#1, with a bounded cast
target instead of `any`, shadow-correct scope tracking, and JSDoc-cast
support for untyped scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant