ivon
ivon

Reputation: 11

forEach couldn't get correct types

declare function a(props: number): number;
declare function b(props: string): string;
declare function c(props: boolean): boolean;

type GenHooks<H extends (...args: any[]) => any> = [H, Parameters<H>]

const obj = {
    a: [a, [1]] as GenHooks<typeof a>,
    b: [b, ['2']] as GenHooks<typeof b>,
    c: [c, [false]] as GenHooks<typeof c>,
} as const

type ObjType = typeof obj;
type Names = keyof typeof obj;

const keys = Object.keys(obj) as Names[];

let out: {
    [K in Names]: ReturnType<ObjType[K][0]>
}

when i try to use forEach, it will error


keys.forEach(name => {
    const [fn, args] = obj[name];
    out[name] = fn(...args) // A spread argument must either have a tuple type or be passed to a rest parameter.(2556)
})

it's types will get

const args: [props: number] | [props: string] | [props: boolean]

and then i get an error message: Type 'string | number | boolean' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'.(2322)

but when don not use forEach, it's ok

const [fn, args] = obj.a;
fn(...args)

Upvotes: 1

Views: 127

Answers (1)

First issue is in this line fn(...args).

Here, fn is a union of three functions ((props: number) => number) | ((props: string) => string) | ((props: boolean) => boolean).

Please read this section:

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

It means that when you are trying to call fn, all types of arguments are intersected instead of unionizing. Hence fn expects number & string & boolean instead of number | string | boolean. Please keep in mind that number & string & boolean is impossible type and it infered as never.

You can find more explanation in my article.

Second issue is in this line out[name]. It is impossible to assign return type of fn(...args) to out[name].

Assume fn(...args) compiles and returns string|number|boolean. In the same way works out[name]. It also expects a union of stirng|number|boolean. Consider this exmaple:

    out[name] = 'str'
    out[name] = 42
    out[name] = true

All three expression causes an error, because TS is unsure about exact value of name because we are inside a loop. name can be a|b|c an all of them expects different types. In order to assign any value to out[name], you should check the type of name.

  if (name === 'a') {
        out[name] = 42 // ok
    }

Please see this question with similar issue and my article about mutations in typescript.

However, there several workarounds.

Upvotes: 1

Related Questions