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

Confused type inference with function overload #31578

Closed
inad9300 opened this issue May 24, 2019 · 6 comments
Closed

Confused type inference with function overload #31578

inad9300 opened this issue May 24, 2019 · 6 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@inad9300
Copy link

TypeScript Version: 3.5.0-dev.20190523

Search Terms: broken type inference / checker in function overloading with generics...

Code

I find it quite difficult to explain the problem in words. In essence, TypeScript is somehow not inferring the generic types correctly. I believe it is best to just look at the code:

interface HTMLElementChildrenMap extends Record<keyof HTMLElementTagNameMap, void | Element[]> {
    hr:  void
    table: (HTMLElementTagNameMap['thead'] | HTMLElementTagNameMap['tbody'] | HTMLElementTagNameMap['tfoot'])[]
}

function h<T extends keyof HTMLElementTagNameMap, E extends HTMLElementTagNameMap[T]>(tag: T, children?: HTMLElementChildrenMap[T]): E
function h<T extends keyof HTMLElementTagNameMap, E extends HTMLElementTagNameMap[T]>(elem: E, children?: HTMLElementChildrenMap[T]): E
function h(tagOrElem: keyof HTMLElementTagNameMap | HTMLElement): HTMLElement {
    return null
}

// Errors caught (expected behavior)
h('hr', [])
h('table', [document.createElement('button')]) 

// Errors not caught
h(document.createElement('hr'), [])
h(document.createElement('table'), [document.createElement('button')])

Playground Link: here

A secondary problem is that the error are very much not helpful in the snippet above. They become useful when the order of the overloads is reversed:

interface HTMLElementChildrenMap extends Record<keyof HTMLElementTagNameMap, void | Element[]> {
    hr:  void
    table: (HTMLElementTagNameMap['thead'] | HTMLElementTagNameMap['tbody'] | HTMLElementTagNameMap['tfoot'])[]
}

function h<T extends keyof HTMLElementTagNameMap, E extends HTMLElementTagNameMap[T]>(elem: E, children?: HTMLElementChildrenMap[T]): E
function h<T extends keyof HTMLElementTagNameMap, E extends HTMLElementTagNameMap[T]>(tag: T, children?: HTMLElementChildrenMap[T]): E
function h(tagOrElem: keyof HTMLElementTagNameMap | HTMLElement): HTMLElement {
    return null
}

h('hr', [])
h('table', [document.createElement('button')])

Playground Link: here

Related Issues: #27972, #20396, #10645, #21707

@MartinJohns
Copy link
Contributor

MartinJohns commented May 24, 2019

In your second overload there is nothing that T can be inferred from. As a result T will fall back to being keyof HTMLElementTagNameMap, and HTMLElementChildrenMap[T] will be a union of all possible combinations. One of those combinations is provided by your table property, which accepts an array of type HTMLElementTagNameMap['thead'] | HTMLElementTagNameMap['tbody'] | HTMLElementTagNameMap['tfoot'] - the empty array [] matches this constraint. The same applies to the second call.

You can see this when hovering over the h in your method invocation and check the tooltip.

@inad9300
Copy link
Author

Thanks for your reply. In my mind, T can be inferred from E quite easily. I was expecting TypeScript to be able to figure this out as well, as from my experience TypeScript's inference power is great.

How can I type this, then, so that there isn't a need to explicitly specify the generic types? Is there a way to somehow have a reversed version of HTMLElementTagNameMap that goes from interface to tag?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label May 24, 2019
@inad9300
Copy link
Author

Could perhaps someone tell me at least if this problem is actually solvable? It might be more obvious for you than it is for me... That would be really helpful already.

@inad9300
Copy link
Author

inad9300 commented Jun 7, 2019

I'm pretty convinced that this problem indeed can't be solved with current TypeScript features. That is because HTMLElementTagNameMap maps multiple tags to the same HTMLElement interface, therefore making it impossible for the compiler to identify whether (e.g.) <abbr> or <em> has been provided. Even then, I believe nominal types would be necessary to be able to write a function like the one above.

Another version of the problem which is also not solvable at the moment (and which I would like to see TypeScript do at some point) is improving the type safety of existing HTML interfaces by restricting the way HTML hierarchies can be formed. For example:

interface HTMLTableElement {
    appendChild<T extends HTMLElementTagNameMap['thead'] | HTMLElementTagNameMap['tbody'] | HTMLElementTagNameMap['tfoot']>(newChild: T): T
}

This could be solved by giving specific names to all HTML element interfaces, e.g. HTMLEmElement for <em> and HTMLAbbrElement for <abbr>. If TypeScript included these declarations, even if they were empty at first, we could then extend them as shown above. My question then being: is this something you would consider doing? (Should I open a new issue?)

@inad9300
Copy link
Author

inad9300 commented Jun 10, 2019

I came across another example which seems to show that type inference is indeed not working properly, which I would like to report as a bug:

interface A {
    __d: 'A'
}

interface B {
    __d: 'B'
}

interface HtmlTypes {
    a: A
    b: B
}

interface HtmlChildren {
    a: void
    b: B[]
}

function h<T extends keyof HtmlTypes, E extends HtmlTypes[T]>(e?: E, c?: HtmlChildren[T]) {}

h(null as A, []) // Type inference not working: no errors reported.
h<'a', A>(null, []) // Explicit types given: errors correctly reported.

Link to Playground.

I was trying to introduce a discriminator field to aid TypeScript, but it doesn't seem to be working as expected. Shouldn't this be possible, in the same way that the following is?

function h2<T extends keyof HtmlTypes, E extends HtmlTypes[T]>(t?: T, c?: HtmlChildren[T]) {}

h2('a', []) // Type inference working: errors correctly reported.

@inad9300
Copy link
Author

Thanks, but no thanks, @typescript-bot.

Could you please re-open this issue until someone has time to look at it, @RyanCavanaugh? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants