Search code examples
typescripttypescript-genericsunion-typesnarrowing

How to pick generics from function in union type dynamically in Typescript?


Lets say I have a function like this:

type FooParams <Params extends unknown[], Result> = { 
 name: string, 
 request: (...params: Params) => Promise<Result> 
}

const foo = <Params extends unknown[], Result>(params: FooParams<Params, Result>) => {
  // do stuff
}

Lets also say I have a couple of requests and a "store" containing those requests:

interface Todo {
  id: number;
  title: string;
}

const getTodos: () => Promise<Todo[]> = () => Promise.resolve([{ id: 2, title: 'clean' }]);

const getTodo: (id: number) => Promise<Todo> = (id: number) => Promise.resolve({ id, title: 'clean' });

const requestStore = {
  getTodo: {
    name: 'getTodo',
    request: getTodo,
  },
  getTodos: {
    name: 'getTodos',
    request: getTodos,
  },
} as const;

I would now like to generate foo-functions for each request in the store.

Adding them manually with explicit key for each request in the store works:

// Works
foo(requestStore['getTodo'])

But adding them dynamically like this does not work:

// Does not work. Error message:
// Type '(() => Promise<Todo[]>) | ((id: number) => Promise<Todo>)' is not assignable to type '() => Promise<Todo[]>'.
//  Type '(id: number) => Promise<Todo>' is not assignable to type '() => Promise<Todo[]>'.(2322)
const createFooFromStore = (requestName: keyof typeof requestStore) => () => {
  const { name, request } = requestStore[requestName]
  foo({ name, request })
}

Is there someway one could re-write this so that a foo-function could be created for each entry in the "requestStore"?

Here is a playground link with the example code:

Playground

In which the "request"-parameter at the very bottom shows an error message.


Solution

  • I cant post this playground link into the comments section... but this works playground

    // The type constraint here does the trick.
    // It allows every specified key in the Requeststore as an input for foo
    // The 
    const foo = <T extends RequestStore[keyof RequestStore]>(params: T) => {
      // return params to check type infering
      return params
    }
    
    interface Todo {
      id: number;
      title: string;
    }
    
    const getTodos: () => Promise<Todo[]> = () => Promise.resolve([{ id: 2, title: 'clean' }]);
    
    const getTodo: (id: number) => Promise<Todo> = (id: number) => Promise.resolve({ id, title: 'clean' });
    
    // if you want to ensure typesafety here you can use the new satisfies keyword to prevent wrong request definitions but you don't need it
    
    const requestStore = {
      getTodo: {
        name: 'getTodo',
        request: getTodo,
      },
      getTodos: {
        name: 'getTodos',
        request: getTodos,
      },
    } as const satisfies Record<string, { name: string, request: (...args: any[]) => any }>;
    type RequestStore = typeof requestStore
    
    // Works
    foo(requestStore['getTodo'])
    foo(requestStore['getTodos'])
    
    
    const createFooFromStore = <T extends keyof typeof requestStore>(requestName: T) => () => foo(requestStore[requestName])
    
    const a = createFooFromStore("getTodo") // valid
    const b = createFooFromStore("getTodos") // valid
    const c = createFooFromStore("getTos") // invalid
    
    const createFooFromStore2 = <T extends keyof typeof requestStore>(requestName: T) => () => { foo(requestStore[requestName]) }