Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem with generics in Signatures #610

Open
gossi opened this issue Aug 8, 2023 · 1 comment
Open

Problem with generics in Signatures #610

gossi opened this issue Aug 8, 2023 · 1 comment

Comments

@gossi
Copy link
Collaborator

gossi commented Aug 8, 2023

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
Copy link
Member

dfreeman commented Aug 9, 2023

It might work to change this line:

? NonNullable<Element> extends never

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants