Reputation: 2281
I have some piece of code like below. I write custom hook with generic type for defined custom type return of my hook.
type Return<T> = [T, (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, Record<keyof T, boolean>, () => void]
function useInput<T>(): Return<T> {
const [input, setInput] = useState<T>({} as T);
const [isDirty, setDirty] = useState({} as Record<keyof T, boolean>);
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setInput({
...input,
[e.target.name]: e.target.value
});
setDirty({
...isDirty,
[e.target.name]: true
});
};
const resetInput = () => {
Object.keys(input).forEach((v) => input[v] = ''); //this line i get error
setInput({...input});
};
return [input, handleInputChange, isDirty, resetInput]
}
export default useInput;
My generic type T
is object. But when I loop over this, I get this error Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
How can I defined type of key for generic type T
? Please help me.
Upvotes: 1
Views: 296
Reputation: 20162
So Object.keys
function is not generic parametrized, by this reason v
in the forEach is always string. Here is reasoning why - Why doesn't Object.keys return a keyof type in TypeScript?.
Thats said you cannot narrow type of v
it will be just string. What you can do is type assertion:
Object.keys(input).forEach((v) => input[v as keyof T] = '')
But you will get next error as you don't define that T
has all values as strings, and you assign to it empty string. This is actually correct error. You cannot assign to every field of unknown object a string.
In order to fix the second we need to narrow the type of value of T
function useInput<T extends Record<string, string>>(): Return<T>
We are saying now T
has keys and values which are string
or subtypes of string
. Because of the fact that they can be subtypes of string we need to do the second assertion after. Why - Subtype can be for example 'a' | 'b'
and such type cannot take empty string value.
Object.keys(input).forEach((v) => (input[v as keyof T] as string) = '')
But without stating that T extends Record<string, string>
we would not able to assert property type of such to string
.
As I said before such implementation has an issue, its risky as if you have object which is for example type A { a: 'x' | 'y'}
and you pass it to such construct, then you can get empty string in property a
and this is invalid member of type A
. But if you work with types which have string
values only, and I assume you do, then its fully ok.
Upvotes: 1