Skip to content

TS2349 and TS2531 in React State: What Each One Means

TS2349 'not callable' and TS2531 'possibly null' in React state, reproduced under TypeScript 7.0.2 with @types/react 19.3: the eleven cases and each fix.

· · 8 min read

Quick Take

Two error codes account for most of the compiler noise around useState and useRef. One says you're calling something that isn't a function; the other says the thing might be null. Here is every variant I could reproduce, the exact diagnostic, and the fix that doesn't involve a non-null assertion.

I wrote eleven small components that each trigger one of these errors, compiled them with typescript 7.0.2 and @types/react 19.3.0 under strict, then ran the same file through typescript 5.9.3. The diagnostics were identical on both compilers, so nothing here depends on which tsc your project is on. Every message below is pasted from the compiler output, not paraphrased.

TL;DR: TS2349 means the thing you're calling isn't a function, and in React that's a swapped useState tuple nine times out of ten. TS2531 means the compiler proved a null is possible and you assigned through it; reads produce its sibling TS18047, and calls produce TS2721 or TS2722. Fix the type, not the symptom: reorder the tuple, narrow the union, or use ?. on the ref.

Which Code Did You Actually Get?

The two codes in the title are the ones people search for, but the compiler hands out five different numbers for what feels like two mistakes. This table is the output of my test file, one row per case:

What the code didDiagnostic
const [setCount, count] = useState(0); setCount(1)TS2349: This expression is not callable. Type 'Number' has no call signatures.
const { count, setCount } = useState(0)TS2339: Property 'count' does not exist on type '[number, Dispatch<SetStateAction>]'.
const [count] = useState(0); count()TS2349: Type 'Number' has no call signatures.
useState<string | (() => void)>('x') then handler()TS2349: Not all constituents of type 'string | (() => void)' are callable.
useReducer state called as a functionTS2349: Type 'Number' has no call signatures.
useRef<HTMLInputElement>(null) then inputRef.current.focus()TS18047: 'inputRef.current' is possibly 'null'.
useState<User | null>(null) then user.name in JSXTS18047: 'user' is possibly 'null'.
document.getElementById('root').textContent = 'hi'TS2531: Object is possibly 'null'.
document.querySelector('.x') then el.classList.add()TS18047: 'el' is possibly 'null'.
useState<(() => void) | null>(null) then onDone()TS2721: Cannot invoke an object which is possibly 'null'.
optional prop onClick?: () => void then onClick()TS2722: Cannot invoke an object which is possibly 'undefined'.

Worth pausing on the TS2531 row. It's the only one. Every other possibly-null read in the file came back as TS18047, which TypeScript introduced to put the variable name in the message. TS2531 survived in exactly one spot: an assignment through a possibly-null object. So if you're seeing TS2531 on a modern compiler, look for a = to the right of the property access. If you're seeing it everywhere, you're on an old TypeScript, and the fixes are the same.

Why Does TS2349 Follow a Swapped useState Tuple?

Because useState returns a tuple, and a tuple is positional. @types/react types it as [S, Dispatch<SetStateAction<S>>], so whatever name you put first is the state and whatever you put second is the setter. Names mean nothing to the compiler.

// What I wrote
const [setCount, count] = useState(0);
return <button onClick={() => setCount(1)}>{count}</button>;
error TS2349: This expression is not callable.
  Type 'Number' has no call signatures.
error TS2322: Type 'Dispatch<SetStateAction<number>>' is not assignable to type 'ReactNode'.

The second error is the tell. If a TS2349 on your setter is paired with a TS2322 about Dispatch<SetStateAction<...>> not being a ReactNode, the tuple is backwards. You tried to render the setter. Swap the names and both errors leave together.

The object-destructuring version produces TS2339 instead, because you asked a tuple for a property it doesn't have. React's docs are consistent about array destructuring for a reason, and it isn't style: the tuple is the API.

The union case is the one I'd call a real design error rather than a typo:

const [handler, setHandler] = useState<string | (() => void)>('x');
return <button onClick={() => handler()}>go</button>;
error TS2349: This expression is not callable.
  Not all constituents of type 'string | (() => void)' are callable.
    Type 'string' has no call signatures.

The compiler is right, and the fix is to admit that state holding either a label or a function was a shortcut. Split it into two pieces of state, or make the union a discriminated union with a kind field you can switch on. typeof handler === 'function' narrows it too, if the shape is forced on you by a library.

Names mean nothing to a tuple: whatever you write first in a useState destructure is the state, whatever you write second is the setter, however you spelled them.

Share this Post on X Bluesky

Why Is ref.current Possibly Null After Mount?

Because the type says so and the compiler can't see React's lifecycle. In @types/react 19.3, useRef<HTMLInputElement>(null) returns RefObject<HTMLInputElement | null>. React fills current during commit, before your effect runs, but that's a runtime fact. The type has no "after mount" state.

const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current.focus(); }, []);
error TS18047: 'inputRef.current' is possibly 'null'.

The honest fix is inputRef.current?.focus(). It's one character more than the bang, and it stays correct the day someone wraps the input in a condition. I reach for if (inputRef.current === null) { return; } when the effect does more than one thing with the element, because a chain of ?. across five lines reads worse than one guard at the top.

What about the bang, inputRef.current!.focus()? It compiles. I'd still not merge it, and here's the opinion part: every non-null assertion in a component is a bet that the null branch never happens, placed by the person who understands the component least well, which is whoever edits it next. The guard costs nothing. The bet costs a production TypeError eventually.

How Do You Render Nullable State Without a Null Check on Every Line?

Push the check up. The user.name error is the common one:

const [user, setUser] = useState<User | null>(null);
return <p>{user.name}</p>;
error TS18047: 'user' is possibly 'null'.

You could write user?.name and move on. For one field, do that. For a component that reads six fields off user, return early instead:

if (user === null) {
  return <p>Loading</p>;
}
return (
  <article>
    <h2>{user.name}</h2>
    <p>{user.id}</p>
  </article>
);

After the early return, user is User for the rest of the function. That's control-flow narrowing, and it's the same mechanism that makes the kind switch work in a discriminated union. Does the loading branch belong in this component at all? Often not. If the data comes from a query library, the TanStack Query guide shows the pattern where the parent owns the null and the child receives a User that can't be null, and the error never appears.

When Is the Null a Callback?

Then the code is TS2721 or TS2722, and the fix is the optional call operator:

export function ButtonH({ onClick }: { onClick?: () => void }) {
  return <button onClick={() => onClick()}>go</button>;
}
error TS2722: Cannot invoke an object which is possibly 'undefined'.

Write onClick?.(). It calls the function if there is one and evaluates to undefined otherwise. For an optional prop that's exactly the semantics you wanted. For state holding a callback, useState<(() => void) | null>(null), the same onDone?.() works, though storing a function in state is itself a smell worth a second look: a function in useState is called by React on initialization if you pass it directly, so you'd need useState(() => fn), and most of the time a useRef or a plain prop is what the code meant.

Why Do These Errors Vanish With strict Off, and Why That's Worse?

Every TS2531, TS18047, TS2721 and TS2722 in the table is a strictNullChecks diagnostic. Turn the flag off and the file compiles, and inputRef.current.focus() throws on the first render where the ref is empty. The error didn't go away. It moved from your terminal to your users.

I ran the eleven cases through the strict-mode ladder to confirm which ones depend on which flag, and the split is clean: TS2349 and TS2339 appear with no strict flags at all, because calling a number is wrong under any configuration. The null family needs strictNullChecks. If your project is mid-migration and you're seeing only the TS2349 kind, that's not a clean bill of health; it's the null checks you haven't turned on yet.

TypeScript 7 changes the default here. On the sixty-line legacy sample from the strict-mode guide, tsc 7.0.2 with no config and no flags reported eight errors; tsc 5.9.3 on the same file reported none. If your first encounter with TS18047 came the week you upgraded, that's why.

How Do You Read the Message Before Searching for It?

Three habits that cut my own debugging time more than any of the fixes above:

  1. Read the second line. TS2349 always names the type that isn't callable, and 'Number' versus 'string | (() => void)' sends you to different fixes.
  2. Look for a paired error on the same line or the next. A TS2349 next to a TS2322 about Dispatch is a swapped tuple; a TS2349 on its own is a call on a value.
  3. Check whether the possibly-null thing is read, assigned or called. Read gives TS18047, assignment gives TS2531, call gives TS2721 or TS2722, and each wants a different operator: ?., a guard, or ?.().

None of this is clever. It's the difference between pasting a code into a search box and fixing the line in the time it takes the page to load.

Frequently Asked Questions

What does TS2349 'This expression is not callable' mean in React?
You are calling a value whose type has no call signature. In React code it almost always means the useState tuple got destructured in the wrong order, so the number ended up in the variable you named setCount, or you called the state value itself. The second line of the message names the type: 'Type Number has no call signatures' points at a state value, 'Not all constituents of type string | (() => void) are callable' points at a union that needs narrowing.
What does TS2531 'Object is possibly null' mean?
The compiler can prove the value may be null and you touched a property on it. Under TypeScript 5.9 and 7.0 that exact code shows up when you assign through a possibly-null object, such as document.getElementById('x').textContent = 'hi'. Reading a property instead gives the newer TS18047, which says 'is possibly null' with the variable name. Same cause, same fixes: narrow with an if, use optional chaining, or stop the null from existing.
Why does ref.current give a null error when I passed the ref to an element?
Because useRef<HTMLInputElement>(null) is typed as RefObject<HTMLInputElement | null> in @types/react 19, and the compiler does not know when React assigns it. Inside an effect that runs after mount the value is set, but nothing in the type system says so. Write inputRef.current?.focus() or guard with an if; both compiled clean in my run.
Is it safe to use the non-null assertion operator to silence TS2531?
It compiles, and that is the whole problem: it moves the crash to runtime. In a component the null case is usually real, the first render before an effect runs, a user that has not loaded yet, a ref on an element that is conditionally rendered. Prefer optional chaining for reads and an early return for the render path. Reserve the bang for the one place you can state in a comment why null is impossible.