Since the type of useValue is Function it makes no sense to pass in generic type arguments. Who do they benefit? The runtime doesn't get them, they are erased at compiler time. The compiler can't use them since Function is just an un-typed function, so there is no benefit there. Passing type arguments is useless and arguably a mistake (ie the user was not expecting this to be Function and is passing in the type arguments thinking they have some effect).
Remove the type arguments and drop the pretense that this is in any way typed:
const { useValue } = useStore()
const [isPending, setIsPending] = useValue('isPending')
const [results, setResults] = useValue('results')
The more interesting question is why are you writing the code like this since there is a way to fully type everything in this code:
const state = {
isPending: false,
results: ['a', 'b', 'c']
}
type State = typeof state;
const useStore = () => {
const useValue = <K extends keyof State>(name: K) => {
const value = state[name]
const setValue = (value: State[K]): void => {
state[name] = value
}
return [value, setValue] as const
}
return { state, useValue }
}
type UseStore = ReturnType<typeof useStore>;
const { useValue } = useStore()
const [isPending, setIsPending] = useValue('isPending')
const [results, setResults] = useValue('results')
The version above is fully type safe and does not require any duplication of names or types (You could of course split this out in multiple files, but then probably some duplication would need to occur depending on your requirements). If this is not applicable in your case, I would be interested to know why.
Edit
If you just want the types to work out on the last lines and have some type safety there, you just need to specify the signature for the functions using generics:
interface UseStore {
state: Record<string, any>
useValue: <T,>(name: string) => [T, (value: T)=> void]
}
const state: Record<string, any> = {
isPending: false,
results: ['a', 'b', 'c']
}
const useStore = (): UseStore => {
const useValue = <T,>(name: string): [T, (value: T)=> void] => {
const value: T = state[name]
const setValue = (value: T): void => {
state[name] = value
}
return [value, setValue]
}
return { state, useValue }
}
const { useValue } = useStore()
const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')
Edit - An open ended in interface implementation
You can define State as an interface, since interfaces are open-ended you can add members when needed. The benefit is if someone else defines a property with the same name but a different type you get an error
interface State {
}
// Don't know what is in here, empty object for starters
const state : State = {
} as State
const useStore = () => {
const useValue = <K extends keyof State>(name: K) => {
const value = state[name]
const setValue = (value: State[K]): void => {
state[name] = value
}
return [value, setValue] as const
}
return { state, useValue }
}
type UseStore = ReturnType<typeof useStore>;
const { useValue } = useStore()
interface State { isPending: boolean }
state.isPending = false; // no guarantee people actually intialize, maybe initial value can be passed to useValue ?
const [isPending, setIsPending] = useValue('isPending')
interface State { results: string[] }
state.results = ['a', 'b', 'c'];
const [results, setResults] = useValue('results')
interface State { results: number[] } // some else wants to use results for something else, err
const [results2, setResults2] = useValue('results')