Why a simple useDarkMode hook tripped up TypeScript

A dark mode hook built on React.useState looked perfectly fine to the human eye, but TypeScript flagged the setMode call as not callable. The error message—"This expression is not callable. Because not all constituents of type 'string | React.Dispatch<SetStateAction<string>>' are callable"—points to the real issue: TypeScript infers the return value of the hook as an array whose elements can be either a string or a dispatch function.

When the hook returns a plain array literal like this:

const array = useDarkMode()
const mode = array[0]
const setMode = array[1]

TypeScript sees the following type:

Array<string | React.Dispatch<React.SetStateAction<string>>>

From TypeScript's perspective, each element of that array is a union of string and React.Dispatch. It has no way to know that the first slot is always the state value and the second is always the setter. Destructuring with const [mode, setMode] = useDarkMode() gives you two variables, each typed as that union—and since a string isn't callable, calling setMode fails type checking.

How React's useState avoids this

React's own type definition for useState doesn't return a generic array. Its return type is explicitly a tuple:

function useState<S>(
	initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>]

That tuple has a fixed length and a fixed type per position. The first element is the state value, the second is the dispatch function. A union-typed array doesn't give any positional guarantees, which is why the hook's return type breaks down.

Fixing the return type

There are a few straightforward ways to tell TypeScript that the array positions are fixed. One option is to declare the function's return type explicitly:

function useDarkMode(): [string, React.Dispatch<React.SetStateAction<string>>] {
	// ...
	return [mode, setMode]
}

Another is to give the local variable a tuple type:

function useDarkMode() {
	// ...
	const returnValue: [string, React.Dispatch<React.SetStateAction<string>>] = [
		mode,
		setMode,
	]
	return returnValue
}

The cleanest option, though, is built into TypeScript itself. Since the compiler already knows what types are flowing into the array, you can just assert that the value is a constant with as const:

function useDarkMode() {
	// ...
	return [mode, setMode] as const
}

That conversion gives the array a tuple-like type, so destructuring works exactly as intended—mode is a string and setMode is a callable dispatch function. No verbose annotations needed.

Going further with literal types

For dark mode specifically, the state only ever takes dark or light. Instead of letting TypeScript infer a generic string, you can pass the possible values explicitly:

function useDarkMode() {
	const [mode, setMode] = React.useState<'dark' | 'light'>(() => {
		// ...
		return 'light'
	})

	// ...

	return [mode, setMode] as const
}

Now setMode will only accept those two strings. This catches mistakes like passing in "Dark" with a capital letter or a misspelled value at compile time. Defining type aliases for the state and dispatch function also makes prop types cleaner when passing these values around the application.