Search code examples
typescripttype-inferencetype-assertion

Use some type constraints on a parameter, while restoring the parameter type for the return value


I have a function createModule which just returns its parameter:

function createModule(obj) {
  return obj
}

The return value must have exactly the type inferred from the parameter:

interface Mod1State {
  p1: string
}

const mod1 = createModule({
  namespaced: true,
  state: {
    p1: "abc"
  } as Mod1State,
  mutations: {
    SET_P1(state, p1: string) {
      state.p1 = p1
    }
  }
} as const)

// 'mod1' must be of type: '{ namespaced: true, state: Mod1State, mutations: { SET_P1(state: any, p1: string): void } }'

So far, it's easy:

function createModule<T>(obj: T): T {
  return obj
}

Now, I would like to add some autocompletion in the parameter state of SET_P1. And I'd rather check the state property instead of casting it.

    SET_P1(state, p1: string) {
      // Here, 'state' should be of type Mod1State
    }

Here is what I tried:

function createModule<S, T extends WithState<S> = WithState<S>>(obj: VuexModule<T, S>): T {
  return obj
}

interface WithState<S> {
  state?: S
}

type VuexModule<T extends WithState<S>, S = T["state"]> = T & {
  namespaced?: boolean
  state?: S
  mutations?: {
    [K: string]: (state: S, payload: any) => void
  }
}

It works only if I remove the as const (I don't understand why):

const mod1 = createModule<Mod1State>({
  namespaced: true,
  state: { // Good: the type of 'state' is checked
    p1: "abc"
  },
  mutations: {
    SET_P1(state, p1: string) { // Good: 'state' is of type 'Mod1State'
      state.p1 = p1
    }
  }
})

But mod1 is now of type WithState<Mod1State>. The inferred type is lost. How to restore the exact type of the createModule's parameter for the type of the return value?

See also: the example in the playground.

EDIT: I obtained something with a code derived from my example. I don't even understand how it works. And why the type of namespaced is inferred as true instead of boolean without as const.


Solution

  • I paste here a solution that seems to work. I'm not totally sure why and how.

    interface Mod1State {
      p1: string
    }
    
    function createModule<
      T extends WithState,
      S = T["state"]
    >(obj: T & VuexModule<S>): T {
      return obj
    }
    
    interface WithState {
      state?: any | (() => any)
    }
    
    interface VuexModule<S> {
      namespaced?: boolean
      state?: S | (() => S)
      mutations?: {
        [K: string]: (state: S, payload: any) => void
      }
    }
    
    const mod1 = createModule({
      namespaced: true,
      state: {
        p1: "abc"
      } as Mod1State,
      mutations: {
        SET_P1(state, p1: string) { // Good: 'state' is of type 'Mod1State'
          state.p1 = p1
        }
      }
    })
    // Good: 'mod1' has the correct type,
    // including 'namespaced: true' instead of 'namespaced: boolean'
    

    Playground