Reputation: 812
I am trying to create a React Hook using TS. As arguments, I am giving the key and the value that will be assigned to this key. I would like the type of the value to be restricted by reading the given key.
export function useFormFields<T, K extends keyof T>(initialValues: T) {
const [formFields, setFormFields] = useState<T>(initialValues)
const createChangeHandler = (key: K, value: T[K]) => {
setFormFields((prev: T) => ({ ...prev, [key]: value }))
}
return { formFields, createChangeHandler }
}
When I use the createChangeHandler function, the key part works, but it combines the types for the value.
Let's say I have an object / interface like the following :
interface User {
name: string,
age: number
}
I would like the crateChangeHandler to throw an error on the next example
createChangeHandler('name', 1)
//Works because the combined type are "string" | "number"
//I'd like to have "string" only
Upvotes: 0
Views: 172
Reputation: 329258
Your problem is that you've got the wrong scope for the K
generic type parameter. When a generic function is called, all its type parameters will be specified right there and then; it is not possible to defer specifying a type parameter until some later time. When you call useFormFields
you know what T
is but not what K
will be. But with your version of the function, K
will be inferred anyway. And since there's nothing there to help the compiler infer, it will be widened to its constraint, which is keyof T
.
Instead you want createChangeHandler()
itself to be generic in K
, and so the type parameter for K
should be moved to its scope:
function useFormFields<T>(initialValues: T) {
const [formFields, setFormFields] = useState<T>(initialValues)
const createChangeHandler = <K extends keyof T>(key: K, value: T[K]) => {
setFormFields((prev: T) => ({ ...prev, [key]: value }))
}
return { formFields, createChangeHandler }
}
This gives the behavior you want:
createChangeHandler("age", 10); // okay
createChangeHandler('name', 1); // error!
// Argument of type 'number' is not assignable to parameter of type 'string'
Upvotes: 2
Reputation: 6153
Your function useFormFields
is generic, but the createChangeHandler
function that is returned from it isn't. By the time you invoke useFormFields
, from the compiler's viewpoint you've fixed those types to be a union. This is what the compiler sees:
const handler: (key: keyof User, value: User[keyof User]) => void =
useFormFields(someUser).createChangeHandler
You need to move the generic onto the returned function in order for the type to be narrowed at the call site of the handler:
export function useFormFields<T>(initialValues: T) {
const [formFields, setFormFields] = useState<T>(initialValues)
const createChangeHandler = <K extends keyof T>(key: K, value: T[K]) => {
setFormFields((prev: T) => ({ ...prev, [key]: value }))
}
return { formFields, createChangeHandler }
}
now the compiler sees:
const handler: <K extends keyof User>(key: K, value: User[K]) => void =
useFormFields(someUser).createChangeHandler;
And it will be able to narrow the value type as you change the key input.
Upvotes: 1