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:
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:
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:
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
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.
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:
A more realistic example — a generic select that curries selection state onto an option component before yielding it:
Current workaround
Extracting the component into a getter with a manual cast preserves
T, but it shouldn't be necessary:Then using
{{component this.optionComponent ...}}instead.Root cause
The emitted TypeScript wraps the first argument to
{{component}}inresolveForBind, which extracts the function signature viaParameters<>/ReturnType<>. This erases generic type parameters — TypeScript collapsesTtounknown(or triggers TS2589 when the type recursion depth is exceeded during the attempt).Related issues
{{#let}}not playing well with components that accept and yield generics #575 — similar problem with string-based lookups (different root cause, labeled as TypeScript limitation)WithBoundArgs(related symptom)Environment
@glint/ember-tsc@1.1.1typescript@5.9.3.gtsfilesPossible fix direction
I've been experimenting with a fix locally. The approach: when
{{component}}receives a direct class reference (PathExpression) with named args, skip theresolveForBindwrapping and pass the class directly to the keyword's type signature. This lets dedicatedComponentKeywordoverloads extract the component's context (args/blocks/element) from the class itself, soTstays intact.The
resolveForBindpath stays unchanged for string lookups, no-arg passthrough, and other keywords (helper/modifier). The change is scoped tocomponent+ class reference + has named args.Happy to open a PR if this direction makes sense, or adjust if you'd prefer a different approach.