Akashii
Akashii

Reputation: 2281

Typescript: How to defined type of object key for generic type

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

Answers (1)

Maciej Sikora
Maciej Sikora

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

Related Questions