Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

incorrect ts suggestions in function overloading

Tags:

typescript

TypeScript compiler is not providing accurate suggestions for the config parameter when calling the fooBar function with the 'view_product' type. While it correctly detects errors when an incorrect key is provided, it fails to enforce the presence of only one key as defined in the FooFn interface. Can I get the correct suggestions in this case?

interface Window {
  fooBar: FooFn
}

interface FooFn {
  (method: 'event', type: 'view_product', config: { view_product_key: any }): void
  (method: 'event', type: 'update_cart', config: { update_cart_key: any }): void
}

window.fooBar('event', 'view_product', {}) // TypeScript incorrectly allows passing an object with more than one key
window.fooBar('event', 'view_product', { update_cart_key: 1 }) // TypeScript correctly flags an error for an incorrect key
window.fooBar('event', 'view_product', { view_product_key: 1 }) // TypeScript correctly allows a valid object with the right key

enter image description here

Playground

like image 313
v.zdorovcev Avatar asked Oct 24 '25 06:10

v.zdorovcev


2 Answers

You've run into a limitation of IntelliSense autosuggest, as reported in microsoft/TypeScript#51047. It looks like overloads that differ by the string literal types of their parameters end up being incorrectly lumped together in the completion lists. Until and unless that issue is resolved, you'll need to either accept this or refactor to avoid overloads.

One way to do this is with generics instead of overloads. You can write a "mapping" interface which represents your parameters as an object structure.

interface ConfigMap {
  event: {
    view_product: { view_product_key: any };
    update_cart: { update_cart_key: any };
  };
}

With that type, you want a FooFn to accept a key of ConfigMap as its first argument, then drill down into that property and have the next argument be a key of that, and then the final argument should be the property value at that nested key. This can be expressed as a call signature that uses generics and indexed access types:

interface FooFn {
  <K1 extends keyof ConfigMap, K2 extends keyof ConfigMap[K1]>(
    method: K1, type: K2, config: ConfigMap[K1][K2]
  ): void    
}

So a FooFn has two generic type parameters. There's K1 for the method argument, which is constrained to the keys of ConfigMap. And there's K2 for the type argument, which is constrained to the keys of ConfigMap[K1] (which is the property type at the K1 key of ConfigMap).

And then config is of type ConfigMap[K1][K2], the type of the property if you drill down into the K1 property of ConfigMap and then the K2 property of ConfigMap[K1].

This behaves more or the same way in terms of accepted and rejected calls:

window.fooBar('event', 'view_product', { update_cart_key: 1 }) // error
window.fooBar('event', 'view_product', { view_product_key: 1 }) // okay

but now the IntelliSense is narrowed so that you only see the appropriate completions:

window.fooBar('event', 'view_product', {});
              suggestion:               ^ view_product_key 

That's because when you write window.fooBar('event', 'view_product', the compiler is able to infer "event" as K1 and "view_product" as K2, and thus the type of config is {view_product_key: any}. There is no trace of update_cart_key left in the call signature to get in the way.

Playground link to code

like image 114
jcalz Avatar answered Oct 25 '25 20:10

jcalz


I'm not gonna be of any help as to why is doesn't work as expected, but may I suggest using Distributive Conditional Types?

The implementation can look like this:

interface Window {
    fooBar: FooFn
}

type ViewProductParams = {
    type: 'view_product',
    view_product_key: any
}

type UpdateCardParams = {
    type: 'update_cart',
    update_cart_key: any
}

interface FooFn {
    (method: 'event', params: ViewProductParams): void
    (method: 'event', params: UpdateCardParams): void
}

window.fooBar('event', { type: 'view_product' }) // error, view_product_key config option is not present
window.fooBar('event', { type: 'view_product', update_cart_key: 1 }) // error, update_cart_key is unexpected for this type
window.fooBar('event', { type: 'view_product', view_product_key: 123 }) // correct type and the view_product_key option is present

You can even remove the overload, since all the param types could be passed as a union (this also gives nicer and more precise errors):

interface FooFn {
    (method: 'event', params: ViewProductParams | UpdateCardParams): void
}
like image 27
Aliser Avatar answered Oct 25 '25 20:10

Aliser



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!