How can I properly set a type for `useState`, if I want the piece of state to either be an object or null?

Viewed 170

I'm having a little trouble using Typescript with React and useState.

Say I have something like,

type TPerson = {
  name: string;
  car: string;
};
export default function App() {
  const [person, setPerson] = React.useState<TPerson>(null);

  const handleClear = React.useCallback(() => {
    setPerson(null);
  }, []);

  const { name, car } = person;

  return (
    <div className="App">
      <h1>
        {person.name} drives a {person.car}
      </h1>
      <button onClick={handleClear}>clear</button>
    </div>
  );
}

This would complain here,

  const handleClear = React.useCallback(() => {
    setPerson(null);
              ^^^^
              Argument of type 'null' is not assignable to parameter of type
  }, []);

Well, ok, let's fix that by changing up TPerson,

type TPerson = {
  name: string;
  car: string;
} | null;

That fixes that error, but now I get a new one,

<h1>
    {person.name} drives a {person.car}
            ^^^^                   ^^^
            Object is possibly 'null'.
</h1>

Well that's easy, I'll just do,

<h1>
    {person?.name ?? 'unknown name'} drives a {person?.car ?? 'unknown car'}
</h1>

But then I get this error,

const { name, car } = person;
        ^^^^  ^^^
        Properties 'name' and 'car' do not exist on type 'TPerson'

Well, yeah, I guess, because TPerson can be null.

I can't even do,

const { name = '', car = '' } = person;

or,

type TPerson = {
  name: string;
  car: string;
};
. . .
const [person, setPerson] = React.useState<TPerson>({} as TPerson);

So what are my options here? If I have a modal that determines it's open state on whether or not person is null, how can I properly go about this?

2 Answers

This isn't a problem with React. It's how typescript works. You need to understand how type narrowing works in typescript to understand this.

// if we have the type below
type MaybePerson = { name: string } | null 

const me: MaybePerson; // me can either be null or an object with the key name

// if we try to access name on me i.e
me.name; // typescript will complain given that `me` can be `null` and `null` doesn't have the property name.

// We can fix this by:-
// 1. If you are sure that me is never null. You can use the non null assertion operator
me!.name

// 2. narrow down the type to person i.e
if(me !== null){
  // me in this block will be an object
  me.name
  
  // you can even destructure me here safely
  const { name } = me
}

Now going back to your react code. One alternative is to render something different when person is null.

type TPerson = {
  name: string;
  car: string;
};

export default function App() {
  // make the type you pass to useState, TPerson | null
  const [person, setPerson] = React.useState<TPerson | null>(null);

  const handleClear = React.useCallback(() => {
    setPerson(null);
  }, []);
 
  if(!person){
    // return some thing else or just null
    return null
  }
  
  // person here will be a TPerson object and you can destructure it safely
  const { name, car } = person;

  return (
    <div className="App">
      <h1>
        {person.name} drives a {person.car}
      </h1>
      <button onClick={handleClear}>clear</button>
    </div>
  );
}

Or you can as well ignore the condition that narrows down the type and specify a default value when person is null i.e

// outside your component
const defaultPerson: TPerson = { name: '', car: '' }

// in your component
const { name, car } = person || defaultPerson;

Also, you can just use the default value instead of ever using null i.e

type TPerson = {
  name: string;
  car: string;
};

const defaultPerson: TPerson = { name: '', car: '' }

export default function App() {
  // use defaultValue
  const [person, setPerson] = React.useState<TPerson>(defaultPerson);

  const handleClear = React.useCallback(() => {
    // set this to defaultPerson
    setPerson(defaultPerson);
  }, []);

  const { name, car } = person;

  return (
    <div className="App">
      <h1>
        {person.name} drives a {person.car}
      </h1>
      <button onClick={handleClear}>clear</button>
    </div>
  );
}

Related