Search code examples
typescripttypescript-typingsreact-typescript

Solving typescript issue where I want a parameter to be one of the values of another type


I'm using React and a store library with Typescript. Here I have an application type:

type ApplicationState = {
  loading: boolean;
  data: string[];
  colors: number[];
  alerts: Alerts;
  error: string;
}

Here I have a "setter" what can change the values for each key in the state individually:

const mySetter = (key: keyof ApplicationState, value: Partial<ApplicationState>) => {
  return {
    ...state,
    [key]: value,
  }
}

Of course, the above throws a type error for the value parameter. The key is simple, I want the keys to be a key of the ApplicationState type. How can I tell typescript that I want the value to be one of the types of ApplicationState (instead of a union type)? I tried Partial and Pick, but typescript still throws errors at me.

Any help is much appreciated.


Solution

  • With the use of generics and indexed access types, this constraint can be enforced in the type signature of mySetter as below:

    <K extends keyof ApplicationState>(key: K, value: ApplicationState[K]) => ApplicationState
    

    Full code below, demonstrating the behaviour you're after:

    type ApplicationState = {
      loading: boolean;
      data: string[];
      colors: number[];
      alerts: any;
      error: string;
    }
    
    const state: ApplicationState = {
      loading: false,
      data: [],
      colors: [],
      alerts: {},
      error: "some error"
    }
    
    const mySetter: <K extends keyof ApplicationState>(key: K, value: ApplicationState[K]) => ApplicationState = (key, value) => {
      return {
        ...state,
        [key]: value,
      }
    }
    
    // The following are happy, value is typed to whatever the type of the key is in ApplicationState type
    mySetter("loading", false)
    mySetter("data", [])
    mySetter("colors", [1, 2])
    
    // The following are unhappy, and will not compile, as desired.
    mySetter("loading", "not-a-boolean-value")
    mySetter("data", "not-an-array-of-strings")
    mySetter("colors", { some: "object" })