Home > OS >  Two levels of generic nesting and Parameters<T> results in a TypeScript error. But one level w
Two levels of generic nesting and Parameters<T> results in a TypeScript error. But one level w

Time:01-23

Here's a minimal example of my problem:

const api = {
    cat1: {
        a: (x: number) => { console.log(x); },
        b: (x: string) => { console.log(x); },
    },
    cat2: {
        c: (x: boolean) => { console.log(x); },
    },
}
type API = typeof api;

const callAPI = <
    Category extends keyof API,
    Name extends keyof API[Category],
>(category: Category, name: Name, param: Parameters<API[Category][Name]>[0]) => {
    console.log(`Param to ${name}: ${param}`);
};

// All these work
callAPI("cat1", "a", 5);
callAPI("cat1", "b", "foo");
callAPI("cat2", "c", false);

// All these error (as they should)
callAPI("cat2", "a", 5);
callAPI("cat2", "b", "foo");
callAPI("cat1", "c", false);
callAPI("cat1", "a", "foo");
callAPI("cat1", "b", false);
callAPI("cat2", "c", 5);

TypeScript playground link

My intention is that the 1st parameter of callAPI is either "cat1" or "cat2". Then, depending on that value, the 2nd parameter can be one of the keys nested inside "cat1" or "cat2". Then, depending on that value, the third parameter is the type of the first parameter of the function inside api.cat1.a (or whatever).

But this produces an error:

Type '{ cat1: { a: (x: number) => void; b: (x: string) => void; }; cat2: { c: (x: boolean) => void; }; }[Category][Name]' does not satisfy the constraint '(...args: any) => any'.
  Type '{ cat1: { a: (x: number) => void; b: (x: string) => void; }; cat2: { c: (x: boolean) => void; }; }[Category][keyof { cat1: { a: (x: number) => void; b: (x: string) => void; }; cat2: { c: (x: boolean) => void; }; }[Category]]' is not assignable to type '(...args: any) => any'.
    Type '{ cat1: { a: (x: number) => void; b: (x: string) => void; }; cat2: { c: (x: boolean) => void; }; }[Category][string] | { cat1: { a: (x: number) => void; b: (x: string) => void; }; cat2: { c: (x: boolean) => void; }; }[Category][number] | { ...; }[Category][symbol]' is not assignable to type '(...args: any) => any'.
      Type '{ cat1: { a: (x: number) => void; b: (x: string) => void; }; cat2: { c: (x: boolean) => void; }; }[Category][string]' is not assignable to type '(...args: any) => any'.

I'm not really sure what that means.

I do know that if I get rid of one level of nesting it works:

const api = {
    a: (x: number) => { console.log(x); },
    b: (x: string) => { console.log(x); },
    c: (x: boolean) => { console.log(x); },
}
type API = typeof api;

const callAPI = <
    Name extends keyof API,
    Param extends Parameters<API[Name]>[0]
>(name: Name, param: Param) => {
    console.log(`Param to ${name}: ${param}`);
};

// All these work
callAPI("a", 5);
callAPI("b", "foo");
callAPI("c", false);

// All these error (as they should)
callAPI("a", "foo");
callAPI("b", false);
callAPI("c", 5);

TypeScript playground link

Or, if I get rid of Parameters<T> it works:

const api = {
    cat1: {
        a: 5,
        b: "foo",
    },
    cat2: {
        c: false,
    },
}
type API = typeof api;

const callAPI = <
    Category extends keyof API,
    Name extends keyof API[Category],
>(category: Category, name: Name, param: API[Category][Name]) => {
    console.log(`Param to ${name}: ${param}`);
};

// All these work
callAPI("cat1", "a", 5);
callAPI("cat1", "b", "foo");
callAPI("cat2", "c", false);

// All these error (as they should)
callAPI("cat2", "a", 5);
callAPI("cat2", "b", "foo");
callAPI("cat1", "c", false);
callAPI("cat1", "a", "foo");
callAPI("cat1", "b", false);
callAPI("cat2", "c", 5);

TypeScript playground link

So overall, I'm not sure what the original error message means, and I'm not sure why I can get rid of it by removing either Parameters<T> or one level of nesting.

CodePudding user response:

TypeScript doesn't always handle constraints on these nested indexes well. You can get around it by declaring a version of Parameters without the constraint (which will just evaluate to never when applied to a non-function type):

type ParametersUnconstrained<T> = T extends (...args: infer P) => any ? P : never

const callAPI = <
    Category extends keyof API,
    Name extends keyof API[Category],
>(category: Category, name: Name, param: ParametersUnconstrained<API[Category][Name]>[0]) => {
    console.log(`Param to ${name}: ${param}`);
};

or you could add a conditional type to remind TypeScript of the constraint: API[Category][Name] extends (...args: any) => any ? API[Category][Name] : never, which corresponds to Extract<API[Category][Name], (...args: any) => any>:

const callAPI = <
    Category extends keyof API,
    Name extends keyof API[Category],
>(category: Category, name: Name, param: Parameters<Extract<API[Category][Name], (...args: any) => any>>[0]) => {
    console.log(`Param to ${name}: ${param}`);
};

And a third option would be to write a conditional type to do the indexing in API:

type IndexAPI<Category, Name> = 
  Category extends keyof API ? Name extends keyof API[Category] ? API[Category][Name] : never : never

which makes callAPI look like this:

const callAPI = <
    Category extends keyof API,
    Name extends keyof API[Category],
>(category: Category, name: Name, param: Parameters<IndexAPI<Category,Name>>[0]) => {
    console.log(`Param to ${name}: ${param}`);
};

Playground link

  •  Tags:  
  • Related