{{component}} with named args on generic class components produces TS2589
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 条评论