typescriptstate

accessing properties using keyof in typescript


Not sure why typescript is complaining here

type ObjectWithMatchingProperties<T> = {
    [K in keyof T]: T[K];
};

type MainState = {
  selectedProduction: {
    eventName: string
  }
  activePricingGroup: {
    name: string
  }
}
type MainStateObj = ObjectWithMatchingProperties<MainState>
type InventoryState = {
  mainSlice: MainStateObj;
};

const state: InventoryState = {
  mainSlice: {
    selectedProduction: {
      eventName: 'test',
    },
    activePricingGroup: {
      name: 'act',
    }
}};

export const useMainInventoryStore = (key: keyof MainState) => state.mainSlice[key]

const selectedProduction = useMainInventoryStore('selectedProduction');
// error at this line:
selectedProduction.eventName

The code Errors out with

Property 'eventName' does not exist on type '(state: InventoryState) => { eventName: string; } | { name: string; }'.

Here is the playgroud


Solution

  • Your useMainInventoryStore function's return type does not depend on its input. Since the input is the union type keyof MainState, then the output is the corresponding union type { eventName: string; } | { name: string; }. Even if you call it with "selectedProduction" as the input, the output is a union type, and TypeScript has no idea whether it will have an eventName property or a name property. And so it complains if you try to index into it with either key (see Typescript property does not exist on union type).


    If you'd like the function's output type to depend on the input type, you should make it a generic function, like this:

    const useMainInventoryStore =
      <K extends keyof MainState>(key: K) => state.mainSlice[key]
    

    Now the input is of generic type K which is constrained to keyof MainState. And thus the output is of the generic type ObjectWithMatchingProperties<MainState>[K], a generic indexed access type corresponding to whatever property exists at state.mainSlice[key].

    And now when you call the function the output type reflects the type of the input:

    const selectedProduction = useMainInventoryStore('selectedProduction')
    // const selectedProduction: {eventName: string;}
    

    And you can index into it:

    selectedProduction.eventName // okay
    

    Playground link to code