typed-ember / glint

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

Problem with generics in Signatures #610

Open gossi opened 1 year ago

gossi commented 1 year ago

I'm providing glint support for ember-element-helper tildeio/ember-element-helper#107

Here are the types based on the ones from Dan (in tildeio/ember-element-helper#102):

export type ElementFromTagName<T extends string> = T extends keyof HTMLElementTagNameMap
  ? HTMLElementTagNameMap[T]
  : Element;

type Positional<T extends string> = [name: T];
type Return<T extends string> = typeof EmberComponent<{
  Element: ElementFromTagName<T>;
  Blocks: { default: [] };
}>;

export interface ElementSignature<T extends string> {
  Args: {
    Positional: Positional<T>;
  };
  Return: Return<T> | undefined;
}

export default class ElementHelper<T extends string> extends Helper<ElementSignature<T>> {}

Using the helper works straight away:

<template>
  {{#let (element 'div') as |Tag|}}
    <Tag id="lalala" ...attributes>Hello there!</Tag>
  {{/let}}
</template>

Going a bit more dynamic and allowing a @tag to be passed in, works when the type is made explicit:

import { type ElementSignature } from 'ember-element-helper';

interface ElementReceiverSignature{
  Element: HTMLDivElement; // 1: explicit element here
  Args: {
    tag: ElementSignature<'div'>['Return']; // 2: explicit tag name here
  };
  Blocks: {
    default: [];
  };
}

export default class ElementReceiver extends Component<ElementReceiverSignature> {
  <template>
    {{#let @tag as |Tag|}}
      <Tag id="content" ...attributes>{{yield}}</Tag>
    {{/let}}
  </template>
}

... of course those explicit types do not make sense, when we want to have any element being passed in. Making it generic makes the problem visible:

import {
  type ElementFromTagName,
  type ElementSignature
} from 'ember-element-helper';

interface ElementReceiverSignature<T extends string> {
  Element: ElementFromTagName<T>;
  Args: {
    tag: ElementSignature<T>['Return'];
  };
  Blocks: {
    default: [];
  };
}

export default class ElementReceiver<T extends string> extends Component<
  ElementReceiverSignature<T>
> {
  <template>
    {{#let @tag as |Tag|}}
      <Tag id="content" ...attributes>{{yield}}</Tag>
    {{/let}}
  </template>
}

This reveals two locations where glint throws both times the same error:

  1. <Tag and
  2. ...attributes

Wit the error message being:

Argument of type 'NonNullable<ElementFromTagName<T>> extends never ? unknown :
ElementFromTagName<T>' is not assignable to parameter of type 'Element'.

  Type 'unknown' is not assignable to type 'Element'.glint(2345)

As to my understanding, the types for the signature are correct, but the error message is wrong. The ElementFromTagName will always return a valid type, in either explicit HTMLElementTagNameMap[T] or generic Element which should be accurate inside the component (typing the unknownigly character of @tag).

Is this a valid problem with glint? Or are my typings wrong?

dfreeman commented 1 year ago

It might work to change this line:

https://github.com/typed-ember/glint/blob/3d4e0685523853702c4b4e54f76dcb726fe89a32/packages/template/-private/signature.d.ts#L48

to something like ? Element extends null

But I don't have the capacity right now to verify that that doesn't adversely affect some other area of inference, so someone else will need to take a look. We should have decent test coverage for this.