typed-ember / glint

TypeScript powered tooling for Glimmer templates
https://typed-ember.gitbook.io/glint
MIT License
109 stars 51 forks source link

Component helper with `{{#let}}` not playing well with components that accept and yield generics #575

Open vstefanovic97 opened 1 year ago

vstefanovic97 commented 1 year ago

Detailed reproduction can be found in this branch where I added the cases here

Files to check are reproduction.${ts|hbs} and options$.{ts|hbs}

Short summary Let's say we have following file

// options.ts
import Component from "@glimmer/component";

interface OptionsComponentSignature<T> {
    Args: {
        options: T[];
    }
    Blocks: {
        default: [options: T]
    }
}

export default class OptionsComponent<T> extends Component<OptionsComponentSignature<T>> {} 

When trying to use it like this

// reproduction.hbs
{{#let (component "options" options=(array 1 2 3)) as |OptionsComponent|}}
  <OptionsComponent as |option|> // option is of type unknown, where actually it should be of type number
    {{option}}
  </OptionsComponent>
{{/let}}
vstefanovic97 commented 1 year ago

After further inspection this problem only seems to happen when passing in a component name as a string to {{component}} helper, if we pass in a component class it works just fine.

After some digging around glint type it seems that when we pass in just a string name it uses this type resolveForBind, and that is causing trouble for -bind-invokable to infer the type.

When passing in a component class, the type being used is resolveForBind which works just fine, I just can't spot the difference in why this one is working whereas the first one is not

dfreeman commented 1 year ago

Unfortunately this is a limitation of what TypeScript is able to do with classes and functions that have type parameters in their signatures. It's not possible purely within the type system itself to preserve generics like this; the only way it's possible to preserve them is in very specific syntactic cases involving runtime function calls.

Glint is designed to use those exact forms to trigger the preservation in TypeScript wherever possible, but unfortunately the dynamicity of string lookups with {{component}} makes that impossible. When resolveForBind receives an actual component (or modifier/helper) value, we're able to preserve the generics, but starting from a string value is one step too far removed from the actual value.

If you're targeting any version of Ember >= 3.27, I'd strongly recommend beginning to migrate away from string lookups with {{component}}/{{modifier}}/{{helper}}. Aside from avoiding this issue, it will also make it easier for you to move to first-class <template> syntax in the future, as string-based lookups are unavailable in strict mode.