Home > OS >  Typescript types of composition Ramda lens with React useState set function
Typescript types of composition Ramda lens with React useState set function

Time:12-13

I am learning FP and I am trying to figure out how to handle events in react. For example, let's use following scenario:

interface Todo {
    task: string
    done: boolean
}

interface TodoProps {
    todo: Todo
    onChange: ChangeEventHandler<HTMLInputElement>
}

function TodoItem({todo, onChange}: TodoProps) {
    return (
        <label>
            {todo.task}
            <input type="checkbox" checked={todo.done} onChange={onChange}/>
        </label>
    )
}

function App() {
    const [todo, setTodo] = useState({
        task: "Some task",
        done: false
    });

    const toggleTodo = // I need to implement this

    return (
        <main>
            <TodoItem onChange={toggleTodo} todo={todo}/>
        </main>
    )
}

Nothing fancy, just basic todo app. In the missing function I need to create object with updated done property. To do that I create Ramda's lens focused on done property.

const doneLens = lensProp('done');

Then it should be easy to finish the goal. All I need is to compose setTodo with Rambda's over.

const toggleDone = () => compose(setTodo, over(doneLens, toggleBoolean))(todo)

But here is the issue. I am getting this ts error:

TS2769: No overload matches this call.   
  The last overload gave the following error.     
    Argument of type '<T>(value: T) => T' is not assignable to parameter of type '(x0: unknown, x1: unknown, x2: unknown) => SetStateAction<{ task: string; done: boolean; }>'.       
      Type 'unknown' is not assignable to type 'SetStateAction<{ task: string; done: boolean; }>'.         
        Type 'unknown' is not assignable to type '(prevState: { task: string; done: boolean; }) => { task: string; done: boolean; }'. 

In pure js this function should work, but ts can't infer return type of over function. It's logical. The over is generic so let's try to add explicit type.

const toggleDone = () => compose(setTodo, over<Todo>(doneLens, toggleBoolean))(todo)

And I get:

TS2554: Expected 3 arguments, but got 2.  
  index.d.ts(669, 51): An argument for 'value' was not provided.

TS2769: No overload matches this call.   
  The last overload gave the following error.     
    Argument of type 'Todo' is not assignable to parameter of type '(x0: unknown, x1: unknown, x2: unknown) => SetStateAction<{ task: string; done: boolean; }>'.       
      Type 'Todo' provides no match for the signature '(x0: unknown, x1: unknown, x2: unknown): SetStateAction<{ task: string; done: boolean; }>'.

Ramda functions are curried by default, but if I can read the error correctly it seem that when I add explicit type then the currying does not working.

I can come up with workaround:

const overDone: (t: Todo) => Todo = over(doneLens, toggleBoolean);
const toggleDone = () => compose(setTodo, overDone)(todo);

This works because return type of overDone match with input type of setTodo.

But, my question is. How to fix the oneliner? Or if you know better way to handle similar scenarios with lenses, function composition, useState hook and typescript I am curious to see so.

CodePudding user response:

Add the type Todo to the lensProp call. You can also move it out of the component, because you don't need to generate the function whenever the component re-renders:

const doneLens = lensProp<Todo>("done");
const toggleDone = over(doneLens, not);

Since setTodo is a function that can call another function (see https://reactjs.org/docs/hooks-reference.html#functional-updates) to which it passes the current todo, you don't need R.compose:

const toggleTodo = () => setTodo(over(doneLens, not));

And this how your component would look (sandbox):

const doneLens = lensProp<Todo>("done");
const toggleDone = over(doneLens, not);

function App() {
  const [todo, setTodo] = useState({
    task: "Some task",
    done: false
  });

  const toggleTodo = () => setTodo(toggleDone);
    
  return (
    <main>
      <TodoItem onChange={toggleTodo} todo={todo} />
    </main>
  );
}

A simpler option would be to use R.evolve (sandbox):

const toggleDone = evolve({ done: not });

function App() {
  const [todo, setTodo] = useState({
    task: "Some task",
    done: false
  });

  const toggleTodo = () => setTodo(toggleDone);

  console.log(todo);

  return (
    <main>
      <TodoItem onChange={toggleTodo} todo={todo} />
    </main>
  );
}
  • Related