Home > database >  Typescript type inference from object
Typescript type inference from object

Time:04-19

I'm trying to get TypeScript to correctly infer typing in the following code example:

type User = {
  name: string
}
type Product = {
  price: number
}

const userConfig = {
  action: () => [{name: 'John'}] as User[],
}
const productConfig = {
  action: () => [{price: 123}] as Product[],
}

const entityConfig = {
  userConfig,
  productConfig,
}

export const makeApp = <K extends string, R>({
  config,
}: {
  config: Record<K, {action: () => R}>
}) => {
  const actions: Record<K, {action: () => R}> = Object.keys(config).reduce(
    (acc, curr) => {
      const c: {action: () => R} = config[curr as K]
      return {
        ...acc,
        [curr]: c.action(),
      }
    },
    {} as any,
  )

  return {actions}
}

const app = makeApp({config: entityConfig})

const users = app.actions.userConfig.action() // Correctly typed as User[]
const products = app.actions.productConfig.action() // Incorrectly typed as User[] instead of Product[]

I see the following TypeScript error on line const app = makeApp({config: entityConfig})

Type '{ userConfig: { action: () => User[]; }; productConfig: { action: () => Product[]; }; }' is not assignable to type 'Record<"userConfig" | "productConfig", { action: () => User[]; }>'.
  The types returned by 'productConfig.action()' are incompatible between these types.
    Type 'Product[]' is not assignable to type 'User[]'.
      Property 'name' is missing in type 'Product' but required in type 'User'.ts(2322)

How can I get the return types for the app.actions.userConfig.action() and app.actions.productConfig.action() calls to be inferred correctly as User[] and Product[] respectively?

CodePudding user response:

You should infer the keys of the passed in object and construct a new interface via { [key in keyof T ]: T[key] }}:

const makeApp = <T, K extends keyof T>({ config }: { config: T }): { actions: { [key in keyof T ]: T[key] } } => {
  const actions: { [key in keyof T ]: T[key] } = Object.keys(config).reduce(
    (acc, curr) => {
      const c = config[curr as K] as unknown as { action: () => void };
      return {
        ...acc,
        [curr]: { action: c.action },
      }
    },
    {} as any,
  )
  return { actions };
}

Because the reducer is not aware that the passed in object properties contain the action key, you can use config[curr as K] as unknown as { action: () => void };.

Playground link.

CodePudding user response:

I'm not sure you've quite asked the right question, or maybe I've misunderstood it. You've asked to start with an object of form { config: entityConfig }, and to do const app = makeApp({config: entityConfig}) for some function makeApp. You then want const products = app.actions.productConfig.action() to work and be correctly typed.

However, this is fairly trivial because entityConfig is already in the right form, so all we need to do is swap our key. It doesn't need a reduce function. The function below works:

const makeApp = <T>({ config }: { config: T }): { actions: T } 
    => ({ actions: config })

Full code:

type User = {
  name: string
}
type Product = {
  price: number
}

const userConfig = {
  action: () => [{name: 'John'}] as User[],
}
const productConfig = {
  action: () => [{price: 123}] as Product[],
}

const entityConfig = {
  userConfig,
  productConfig,
}

const makeApp = <T>({ config }: { config: T }): { actions: T } 
    => ({ actions: config })

const app = makeApp({config: entityConfig})

const users = app.actions.userConfig.action() // Correctly typed as User[]
const products = app.actions.productConfig.action()  // Correctly typed as Product[]

console.log(users[0].name) // John
console.log(products[0].price) // 123
  • Related