Typescript won’t spread generic parameters into matching function

I have an object that contains functions:

const bazApi = {
  fun1: () => string,
  fun2: (n: number) => void,
  fun3: (s: string) => Promise<number>,
}

Then I have a function that takes a key of that object and a tuple of parameters matching the input of the associated function, i.e. fun2 and [42]. The function then uses the identifier to get the function from the object and calls it using the provided arguments. However, Typescript complains:

A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)

I have read up on the error, but I don’t think it is the underlying problem, which is rather TS suddenly not being able to match function and parameters.

Here is the code (and here it is at the playground):

type BazApiInterface = {
    fun1: () => string,
    fun2: (n: number) => void,
    fun3: (s: string) => Promise<number>,
}

type Message<T extends Record<K, (...args: any) => any>, K extends keyof T> = {
    command: K,
    args: Parameters<T[K]>
}

type ApiListener<T extends Record<keyof T, (...args: any) => any>> = <K extends keyof T>(
    message: Message<T, K>
) => void

declare const bazApi: BazApiInterface
const listener: ApiListener<BazApiInterface> = (message) => {
    const { command, args } = message;
    const handler = bazApi[command];    // TS says handler is function from BazApiInterface indexed by CommandName (good)
    const res = handler(...args);       // TS says handler is union over (in TS 4.7.4) or a merge of (?) all function from BazApiInterface (not good)
};

Interestingly, in the line above the error, where handler is set, typescript seems to know that it is the function from the object identified by the command name, i.e. BazApiInterface[K], and I can even get it to write it out as

const handler: ApiInterface<{
  fun1: () => string;
  fun2: (n: number) => void;
  fun3: (s: string) => Promise<number>;
}>[CommandName]

Also, args gives similar output, TS seems to know that it contains the parameters for the function identified by CommandName. So it seems like TS should be able to figure out that they match.

However, when actually calling the function in the next line, TS sees handler either as a union type over all functions in the object (in TS v4.7.4) or as some weird amalgamation of the functions ((arg0: never) => string | void | Promise<number>) in TS v4.9.5, and then correctly complains that something isn’t right. But it would be if the function type hadn’t changed, wouldn’t it?

Currently, I just cheat (const res = (handler as Function)(...args);), but I would really like to know why it happens and if I can do anything about it?

Feels like this question is asked every other day, but I couldn’t find an explanation. This answer and the linked issue in TS’ Github seems very close, but I don’t think it applies (or at least I can’t).

8

The problem you’re having with

const res = handler(...args);

Is that the compiler can’t follow the correlation between the type of handler and the type of args. The error message about spread/rest is misleading, see microsoft/TypeScript#47615. The Parameters<T> utility type is implemented as a conditional type and is thus deferred when its argument is generic. While a human being can read func(...args) where func is of generic type F and args is of generic type Parameters<F> and say “yeah that works”, the compiler doesn’t see it that way, since it doesn’t really know the purpose of Parameters. You can only call func(...args) generically if the compiler knows that args is of type A and func is of type (...args: A) => any.


If you want to try refactoring so the compiler does follow the correlation, you can indeed use the technique mentioned in microsoft/TypeScript#47109, where we try to represent things in terms of generic indexed accesses into mapped types.

For example:

declare const bazApi: BazApiInterface

type Api<T extends Record<keyof T, (...args: any) => any>> =
    { [K in keyof T]: (...args: Parameters<T[K]>) => ReturnType<T[K]> }

const listener: ApiListener<BazApiInterface> =
    (message) => {
        const _bazApi: Api<BazApiInterface> = bazApi;
        const { command, args } = message;
        const handler = _bazApi[command];
        const res = handler(...args); // okay
    };

The Api<T> type is essentially an identity function on T, more or less, and indeed Api<BazInterface> is equivalent to BazInterface. And the compiler is happy to allow you to assign bazApi to _bazApi. But since Api<T> is a mapped type, then so is the type of _bazApi. So _bazApi[command] is now an indexed access into a mapped type, which the compiler can directly evaluate as being type (...args: Parameters<BazInterface[K]>) => ReturnType<BazInterface[K]>. And since args is of type Parameters<BazInterface[K]>, the call succeeds.

Again, it might seem silly that you’re rewriting BazInterface to an equivalent type and now things work, but you can think of it as leading the compiler through the exercise of understanding the generic relationship between handler and args.

Playground link to code

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật