285

I'm migrating a React with TypeScript project to use hooks features (React v16.7.0-alpha), but I cannot figure out how to set typings of the destructured elements.

Here is an example:

interface IUser {
  name: string;
}
...
const [user, setUser] = useState({name: 'Jon'});

I want to force user variable to be of type IUser. My only successful trial, is doing it in two phases: Typing, then initializing:

let user: IUser;
let setUser: any;
[user, setUser] = useState({name: 'Jon'});

But I'm sure there is a better way. Also, setUser should be initialized as a function that takes a IUser as input, and returns nothing.

Also, worth noting that using const [user, setUser] = useState({name: 'Jon'}); without any initialization works fine, but I would like to take advantage of TypeScript to force type checking on init, especially if it depends on some props.

Thanks for your help.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Hassen
  • 6,966
  • 13
  • 45
  • 65

5 Answers5

577

Use this

const [user, setUser] = useState<IUser>({name: 'Jon'});

See the Corresponding Type in DefinitelyTyped

KyleMit
  • 30,350
  • 66
  • 462
  • 664
Nurbol Alpysbayev
  • 19,522
  • 3
  • 54
  • 89
  • This works, but seems totally insensitive to what's provided in place of `IUser`. I can put just about anything there and subsequent uses of `setUser` are passed by type checking. – orome Aug 26 '19 at 18:53
  • 3
    @orome No, you can't put anything there, you can only put there an object that is compatible with `IUser`, i.e. has the same properties. It's called duck typing. – Nurbol Alpysbayev Nov 20 '19 at 22:10
  • Do you how to do the same when my state variable is an array of objects? – João Marcos Gris Nov 21 '19 at 17:54
  • 2
    @JoãoMarcosGris `type MyType = MyObj[]; ` then `useState` – Nurbol Alpysbayev Nov 21 '19 at 19:42
  • so underneath React's hood, it's using these types from DefinitelyTyped? – stackjlei Jul 23 '20 at 22:47
  • @NurbolAlpysbayev - what about the types for `[user, setUser]`? ... aren't we missing out on type-safety if we don't type them? – Joey Baruch Aug 24 '20 at 17:27
  • 2
    @JoeyBaruch No, we aren't :-) Just try it. Then look at the type definition, and you'll see that `useState` returns a well-typed tuple, that is assigned to `[user, setUser]` and it's not hard for TypeScript to understand that the variables should be of the same types as the tuple constituents. Don't know if I cleared things up or confused you further. – Nurbol Alpysbayev Aug 25 '20 at 03:44
46

First useState takes a generic, which will be your IUser. If you then want to pass around the second destructured element that is returned by useState you will need to import Dispatch. Consider this extended version of your example that has a click handler:

import React, { useState, Dispatch } from 'react';

interface IUser {
  name: string;
}

export const yourComponent = (setUser: Dispatch<IUser>) => {

    const [user, setUser] = useState<IUser>({name: 'Jon'});

    const clickHander = (stateSetter: Dispatch<IUser>) => {
        stateSetter({name : 'Jane'});
    }

    return (
         <div>
            <button onClick={() => { clickHander(setUser) }}>Change Name</button>
        </div>
    ) 
}

See this answer.

cham
  • 8,666
  • 9
  • 48
  • 69
4

You could also declare the initial state before and then be able to call it any time you want:

type User = typeof initUser;
const initUser = {name: 'Jon'}
...
const [user, setUser] = useState<User>(initUser);

About I interface prefixes: https://basarat.gitbooks.io/typescript/content/docs/styleguide/styleguide.html#interface

1

https://fettblog.eu/typescript-react/hooks/

// import useState next to FunctionComponent
    import React, { FunctionComponent, useState } from 'react';
    
    // our components props accept a number for the initial value
    const Counter:FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
      // since we pass a number here, clicks is going to be a number.
      // setClicks is a function that accepts either a number or a function returning
      // a number
      const [clicks, setClicks] = useState(initial);
      return <>
        <p>Clicks: {clicks}</p>
        <button onClick={() => setClicks(clicks+1)}>+</button>
        <button onClick={() => setClicks(clicks-1)}>-</button>
      </>
    }
zloctb
  • 10,592
  • 8
  • 70
  • 89
1
class Form {
    id: NullNumber = null;
    name = '';

    startTime: NullString = null;
    endTime: NullString = null;

    lunchStart: NullString = null;
    lunchEnd: NullString = null;

    [key: string]: string | NullNumber;
}

export const EditDialog: React.FC = () => {
    const [form, setForm] = useState<Form>(new Form());


    const inputChange = (e: ChangeEvent<HTMLInputElement>) => {
        const element = e.target;
        setForm((form: Form) => {
            form[element.name] = element.value;
            return form;
        })
    }
    return (
        <Box pt={3}>
            <TextField
                required
                name="name"
                label="Наименование"
                defaultValue={form.name}
                onChange={inputChange}
                fullWidth
            />
        </Box>
    );
}
Code8525
  • 39
  • 3