ITADN

{{component}} with named args on generic class components produces TS2589

#1068Openhlorellium 创建于 2026-03-10
H
hlorelliumcommented
When using `{{component}}` to curry named args onto a generic class component, the type checker produces TS2589 ("Type instantiation is excessively deep and possibly infinite"). ## Reproduction Minimal example: ```gts import Component from '@glimmer/component'; import type { WithBoundArgs } from '@glint/template'; class PickerOption<T> extends Component<{ Args: { value: T; onSelect: (value: T) => void }; Blocks: { default: [T] }; }> { <template>{{yield @value}}</template> } class Picker<T> extends Component<{ Args: { onSelect: (value: T) => void }; Blocks: { default: [WithBoundArgs<typeof PickerOption, 'onSelect'>] }; }> { <template> {{! TS2589: Type instantiation is excessively deep and possibly infinite. }} {{yield (component PickerOption onSelect=@onSelect)}} </template> } ``` A more realistic example — a generic select that curries selection state onto an option component before yielding it: ```gts import Component from '@glimmer/component'; import type { ComponentLike, WithBoundArgs } from '@glint/template'; import { hash } from '@ember/helper'; interface SelectOptionSignature<T> { Args: { model: T; isSelected: boolean; didClick: (model: T) => void; }; Blocks: { default: [] }; Element: HTMLDivElement; } class SelectOption<T> extends Component<SelectOptionSignature<T>> { <template> <div ...attributes role="option" aria-selected={{@isSelected}}> {{yield}} </div> </template> } class Select<T> extends Component<{ Args: { selected?: T; onChange: (model: T) => void; }; Blocks: { default: [{ Option: WithBoundArgs< ComponentLike<SelectOptionSignature<T>>, 'isSelected' | 'didClick' >; }]; }; }> { <template> {{! TS2589 on the component helper call }} {{yield (hash Option=(component SelectOption isSelected=false didClick=@onChange))}} </template> } ``` ## Current workaround Extracting the component into a getter with a manual cast preserves `T`, but it shouldn't be necessary: ```ts get optionComponent(): ComponentLike<SelectOptionSignature<T>> { return SelectOption as ComponentLike<SelectOptionSignature<T>>; } ``` Then using `{{component this.optionComponent ...}}` instead. ## Root cause The emitted TypeScript wraps the first argument to `{{component}}` in `resolveForBind`, which extracts the function signature via `Parameters<>` / `ReturnType<>`. This erases generic type parameters — TypeScript collapses `T` to `unknown` (or triggers TS2589 when the type recursion depth is exceeded during the attempt). ## Related issues - #575 — similar problem with string-based lookups (different root cause, labeled as TypeScript limitation) - #661 — TS2589 with `WithBoundArgs` (related symptom) ## Environment - `@glint/ember-tsc@1.1.1` - `typescript@5.9.3` - Strict mode `.gts` files ## Possible fix direction I've been experimenting with a fix locally. The approach: when `{{component}}` receives a direct class reference (PathExpression) with named args, skip the `resolveForBind` wrapping and pass the class directly to the keyword's type signature. This lets dedicated `ComponentKeyword` overloads extract the component's context (args/blocks/element) from the class itself, so `T` stays intact. The `resolveForBind` path stays unchanged for string lookups, no-arg passthrough, and other keywords (helper/modifier). The change is scoped to `component` + class reference + has named args. Happy to open a PR if this direction makes sense, or adjust if you'd prefer a different approach.
2 条评论