Calculate type of value dynamically based on key property in function

Viewed 69

I have custom hook that adds to useStata function that accepts key and value props;

import { Dispatch, SetStateAction, useCallback, useState } from 'react';

export type HandleModelChangeFn<T> = (key: keyof T, value: T[typeof key]) => void;

const useModel = <T>(initialModel: T): [T, HandleModelChangeFn<T>, Dispatch<SetStateAction<T>>] => {
  const [model, setModel] = useState<T>(initialModel);

  const handleChange = useCallback((key: keyof T, value: T[typeof key]) => {
    setModel((prevState) => ({
      ...prevState,
      [key]: value,
    }));
  }, []);

  return [model, handleChange, setModel];
};

export default useModel;

Problem is that when i use that function second parameter can be any type which exists in interface, example

interface User {
  name: string;
  age: number;
}

const initialModel: User = {
  name: '',
  age: 1
}

const [model, handleModelChange] = useModel(initialModel);

handleModelChange('age', 'John'); // should get error cause value is wrong type

key param can be 'name' or 'age', but second param has type string | number;

I want to calculate type dynamically based on key property, like when key param is 'age' ts will tell me that i passing wrong value type, cause age type is number;

Is there way to do this with typescript?

2 Answers

I would say you just pass an object as the parameter to the useCallback function.

...
// I use the partial interface so you can pass the keys individually
const handleChange = useCallback(obj: Partial<User>) => {
  setModel((prevState) => Object.assign(prevState, obj));
}, []);
...

That way typescript expects only the name and age properties from the object and what type to expect when either one is used.

handleModelChange({ age: 'John' }); // Error
handlModelChange({ name: 'John' }) // Success

YOu need to make a very small change in HandleModelChangeFn. You need to infer a key:

import { Dispatch, SetStateAction, useCallback, useState } from 'react';

// added ----------------------->  <Key extends keyof T>
export type HandleModelChangeFn<T> = <Key extends keyof T>(key: Key, value: T[Key]) => void;

const useModel = <T,>(initialModel: T): [T, HandleModelChangeFn<T>, Dispatch<SetStateAction<T>>] => {
  const [model, setModel] = useState<T>(initialModel);

  const handleChange = useCallback((key: keyof T, value: T[typeof key]) => {
    setModel((prevState) => ({
      ...prevState,
      [key]: value,
    }));
  }, []);

  return [model, handleChange, setModel];
};

export default useModel;


interface User {
  name: string;
  age: number;
}

const initialModel: User = {
  name: '',
  age: 1
}

const [model, handleModelChange] = useModel(initialModel);

handleModelChange('age', 'John'); // error
handleModelChange('age', 2); // ok

Playground

Related