stencil icon indicating copy to clipboard operation
stencil copied to clipboard

Generic components fail to typecheck

Open runarberg opened this issue 3 years ago • 6 comments

Stencil version:

 @stencil/[email protected]

I'm submitting a:

[x] bug report [ ] feature request [ ] support request => Please do not submit support requests here, use one of these channels: https://stencil-worldwide.herokuapp.com/ or https://forum.ionicframework.com/

Current behavior:

When you try to compile a component that has a generic type based on a property type it fails to compile:

[ ERROR ]  TypeScript: ./src/components.d.ts:152:17
           Cannot find name 'Data'.

    L151:   */
    L152:  "data": Data[];
    L153:  /**

Expected behavior:

The component should compile without issues

Steps to reproduce:

Related code:

If I have a component, say a table-like component that operates on generic types (e.g. emits events of said types), is it possible to make it generic?

@Component(/* ... */)
export class MyTable<Data> {
  @Prop() data: Data[] = [];

  get result(): Data[] {
    // data will be sorted, filtered and sliced
  }

  // Pass the result to parent.
  @Event() update: EventEmitter<Data[]>
}

Now when a consumer sets the data property of <my-table> to say Pokemon you would expect e.g. the update event to reflect that.

type Pokemon = {
  name: string;
  type: string;
  cp: number;
 };

const pokemon: Pokemon = [
  { name: 'Pikachu', type: 'Electric', cp: 500 },
  { name: Magikarp, type: 'Water', cp: 23 },
];

myTable.data = pokemon;
myTable.addEventListener('update', (event) => {
  // `event.detail` and `event.target.result` should be of type `Pokemon[]`
});

Other information:

This is only a minor annoyance as it is trivial to use unknown in this case. But it could offer a nice quality of life if fixed.

runarberg avatar Apr 28 '21 11:04 runarberg

I think this may be more a limitation of TypeScript than anything else, but I agree it'd be pretty slick if TS became smart enough to infer a more specific type for myTable based on the data property assignment there.

Some alternatives that may work:

  • ensure that myTable is typed as MyTable<Pokemon> via type declaration, or cast, etc.
  • use a factory function to instantiate MyTable<T>. Something like createTable(props: MyTableProps<T>): MyTable<T>
  • you might get accurate type inference if you use instantiate the component via JSX. Not sure about that one though.

edit: "Contextual Narrowing for Generics" is on the TS roadmap, but as far as I can tell it's just for control flow analysis, which wouldn't help here.

cymptom avatar Apr 28 '21 13:04 cymptom

I see so before contextual narrowing I would need to typecast after a fetch the element from the dom:

const myTable = document.querySelector('my-table') as MyTable<Pokemon>;

However my main issue is that the generated components.d.ts doesn’t add the generic annotation to the interface:

interface FlTable {
  "data": Data[];
}

But what we want is:

-  interface FlTable {
+  interface FlTable<Data> {
     "data": Data[];
   }

I wouldn’t mind if other instances are set to unknown by default:

    var HTMLFlTableElement: {
        prototype: HTMLFlTableElement<unknown>;
        new <Data>(): HTMLFlTableElement<Data>;
    };

etc.

What I’m mainly looking for is an internal consistency in the implementation (e.g. I want to refer to the generic type in a provided sort property, filter property etc.)

Again this is not something that is desperately needed needed. Annotating with unknown works fine, it just feels a little dirty.

runarberg avatar Apr 28 '21 17:04 runarberg

However my main issue is that the generated components.d.ts doesn’t add the generic annotation to the interface

Oh, my bad -- I didn't notice that part of the problem.

As you've indicated, generic types for class components don't seem to be supported, which is pretty unfortunate 😞 I can't think of any good reason why they couldn't be supported, but perhaps someone who knows better can chime in.

I guess there's always Functional Components 🤷‍♂️

cymptom avatar Apr 28 '21 17:04 cymptom

I ran into this as well today - specifically when trying to output a react component. I'd like to be able to pass the generic type to infer it's value type. here's my example:

export class RadioNested<T> 

Then when consuming in my react application I CANNOT do this:

<TnRadioNested<string> 
/>

Because the generic is missing from the components.d.ts:

interface TnRadioNested {
        "headerTitle"?: string;
        "onDidDismiss"?: (event: CustomEvent<T>) => void;
        "onRadioChange"?: (event: CustomEvent<{ selectedRadioValue: string }>) => void;
        "open"?: boolean;
        "options": RadioNestedOption<T>[];
        "popoverFromElementId"?: string;
        "selectedRadioValue"?: string;
    }

The react output target would also need to allow for the generic for my implementation to work

corysmc avatar May 27 '21 21:05 corysmc

Sorry this is somewhat unrelated, but I ended up back here while researching a separate issue that overlaps a bit with the initial example:

myTable.addEventListener('update', (event) => {
  // `event.detail` and `event.target.result` should be of type `Pokemon[]`
});

Even without the use of generics, I don't see accurate type inference here for the event type. It's always any. It looks to me like the compiler outputs event typings for JSX, but not for the DOM Element APIs that come into play when using addEventListener and removeEventListener.

Is this everyone's experience, or am I missing something?

cymptom avatar Jun 15 '21 14:06 cymptom

https://github.com/ionic-team/stencil/blob/16b8ea4dabb22024872a38bc58ba1dcf1c7cc25b/src/compiler/transformers/static-to-meta/parse-static.ts#L32

https://github.com/ionic-team/stencil/blob/16b8ea4dabb22024872a38bc58ba1dcf1c7cc25b/src/compiler/build/build-ctx.ts#L8

https://github.com/ionic-team/stencil/blob/fa38ada12cc102d0539c24092db2e0ef437cdf6c/src/compiler/transpile/run-program.ts#L14

https://github.com/ionic-team/stencil/blob/267e3dd03887eafa7b58f4d4efae9bc833cd6581/src/compiler/types/generate-app-types.ts#L67

https://stackoverflow.com/questions/57521951/how-to-get-generic-type-from-a-method-signature-return-type

We must find the class signature and add it to the componentClassName.

deleonio avatar Jul 23 '22 04:07 deleonio