Reputation: 53
I want to get some object and modify it by keys as strings and return object.
const useInputs = <T extends Record<string, string>>(defaultValue: T) => {
const [inputsData, setInputsData] = useState<T>(defaultValue)
type UpdateFunction = (key: string) => (el: React.FocusEvent<HTMLInputElement>) => void
const updateData: UpdateFunction = key => el => {
const newInputsData = {...inputsData}
newInputsData[key] = el.target.value
setInputsData(newInputsData)
}
return { inputsData, updateData }
}
but I have error on newInputsData[key] = el.target.value
"Type 'string' cannot be used to index type 'T'.ts(2536)"
How I can fix this? Thank you
Upvotes: 2
Views: 1209
Reputation: 31863
The error comes about because the generic type T
may be instantiated with any type that extends Record<string, string>
.
Consider the type
interface Inputs {x: string; y: string}
While Inputs
extends Record<string, string>
, it would be completely unsafe to allow indexing into it with arbitrary strings.
In other words, we wouldn't want the following
const i: Inputs = {x: "A", y: "B"};
const [inputs, setInputs] = useInputs(i);
setInputs("name")(event);
because Inputs
doesn't have a name
property.
Therefore, your inner type, UpdateFunction
, must take an argument that is a valid key of the whatever type T
is instantiated with for any instantiation. Such a type is written keyof T
as in (key: keyof T) => (el: React.FocusEvent<HTMLInputElement>) => void
.
Putting it together it should look like this
const useInputs = <T extends Record<string, string>>(defaultValue: T) => {
const [inputsData, setInputsData] = useState<T>(defaultValue);
type UpdateFunction = (key: keyof T) => (el: {target: {value: T[keyof T]}}) => void;
const updateData: UpdateFunction = key => el => {
const newInputsData = {...inputsData};
newInputsData[key] = el.target.value;
setInputsData(newInputsData)
};
return [inputsData, updateData] as const;
};
Note the adjustment in the return value of the outer function which allows for the conventional use of your hook.
const [inputs, setInputs] = useInputs(...);
as opposed to the unorthodox
const {inputsData, updateData} = useInputs(...);
Further, note that in order to make the above typecheck, we've also had to adjust the type of the second parameter, el
, which provides the value to update the property with from React.FocusEvent<HTMLInputElement>
to {target: {value: T[keyof T]}}
for the same reason discussed above, the generic T
may be instantiated with properties whose values are subtypes of string
such as "despair"
and "elation"
.
Alternatives include using a more precise type for el
such as
Omit<React.FocusEvent<HTMLInputElement>, "target"> & {
target: { value: T[keyof T] };
}
Moving changing how useInput
is generic, from the type of the entire input object, to the just the property keys of that object.
const useInputs = <K extends string>(defaultValue: Record<K, string>) => {
const [inputsData, setInputsData] = useState(defaultValue);
type UpdateFunction = (
key: K
) => (el: React.FocusEvent<HTMLInputElement>) => void;
const updateData: UpdateFunction = key => el => {
const newInputsData = { ...inputsData };
newInputsData[key] = el.target.value;
setInputsData(newInputsData);
};
return [inputsData, updateData] as const;
};
I prefer this last, because HTMLInputElement
's value
is always a string
.
Upvotes: 2