Typing React Components Without Fighting TypeScript

Before adding types, here’s a typical untyped React component:

const operations = {
	'+': (left, right) => left + right,
	'-': (left, right) => left - right,
	'*': (left, right) => left * right,
	'/': (left, right) => left / right,
}

function Calculator({ left, operator, right }) {
	const result = operations[operator](left, right)
	return (
		<div>
			<code>
				{left} {operator} {right} = <output>{result}</output>
			</code>
		</div>
	)
}

const examples = (
	<>
		<Calculator left={1} operator="+" right={2} />
		<Calculator left={1} operator="-" right={2} />
		<Calculator left={1} operator="*" right={2} />
		<Calculator left={1} operator="/" right={2} />
	</>
)

You might prefer a more concise version:

const Calculator = ({ left, operator, right }) => (
	<div>
		<code>
			{left} {operator} {right} ={' '}
			<output>{operations[operator](left, right)}</output>
		</code>
	</div>
)

The implicit return in that style prevents you from declaring variables or using hooks, so it’s not suitable beyond trivial components. A function expression is another option:

const Calculator = ({ left, operator, right }) => {
	const result = operations[operator](left, right)
	return (
		<div>
			<code>
				{left} {operator} {right} = <output>{result}</output>
			</code>
		</div>
	)
}

That works, but function declarations give you better hoisting behavior, which is often preferable.

Why React.FC Falls Short

When you start adding types, the most obvious route is the built-in generic from @types/react:

const Calculator: React.FC<CalculatorProps> = ({ left, right, operator }) => {
	// implementation clipped for brevity
}

This compiles fine but introduces three real issues:

  • The component silently accepts a children prop even when it doesn’t use one. This compiles without error: <Calculator left={1} operator="+" right={2}>What?</Calculator>.
  • Generics are not supported with this pattern.
  • You’re forced into a function expression, ruling out function declarations.

In essence, a React component is just a function that returns something React can render — nothing more special than that. So there’s no need for a special component type at all. The straightforward function typing approach avoids every problem above.

A Simpler Approach to Props

Start with a plain type for the props. The component’s input is just a function argument:

function Calculator({ left, operator, right }: CalculatorProps) {
	// implementation clipped for brevity
}

This gives you no extra props, keeps generics available, and works with either function form. There are no strings attached.

Why Not Specify a Return Type?

You could annotate the return as React.ReactElement or React.ReactNode, but that can easily make the type too broad. Unless you have a good reason, leave the return type to inference — the same default you’d apply to any function.

Narrowing the operator Prop

Our earlier CalculatorProps looks reasonable at first:

// I took the liberty of typing each of these functions as well:
const operations = {
	'+': (left: number, right: number): number => left + right,
	'-': (left: number, right: number): number => left - right,
	'*': (left: number, right: number): number => left * right,
	'/': (left: number, right: number): number => left / right,
}

type CalculatorProps = {
	left: number
	operator: string
	right: number
}
function Calculator({ left, operator, right }: CalculatorProps) {
	const result = operations[operator](left, right)
	return (
		<div>
			<code>
				{left} {operator} {right} = <output>{result}</output>
			</code>
		</div>
	)
}

The left and right numbers are fine, but using string for operator is too loose. The supported operations are limited, and a bad value will cause a runtime error when you do something like:

const element = <Calculator left={1} operator="wut" right={2} />

With TypeScript’s strict mode on, accessing operations[operator] with an arbitrary string is a compile error, since the result isn’t necessarily callable.

A simple union type restricts the operator:

type CalculatorProps = {
	left: number
	operator: '+' | '-' | '*' | '/'
	right: number
}

However, adding a new operator — like the exponentiation operator (**) — would require updating both the operations object and the union type. Instead, you can derive the type straight from the object:

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}

typeof operations gives you the shape of the object, roughly:

type operations = {
	'+': (left: number, right: number) => number
	'-': (left: number, right: number) => number
	'*': (left: number, right: number) => number
	'/': (left: number, right: number) => number
}

Then keyof extracts all its keys, producing '+' | '-' | '*' | '/' automatically.

Here’s the finished component with the derived operator type:

const operations = {
	'+': (left: number, right: number): number => left + right,
	'-': (left: number, right: number): number => left - right,
	'*': (left: number, right: number): number => left * right,
	'/': (left: number, right: number): number => left / right,
}

type CalculatorProps = {
	left: number
	operator: keyof typeof operations
	right: number
}

function Calculator({ left, operator, right }: CalculatorProps) {
	const result = operations[operator](left, right)
	return (
		<div>
			<code>
				{left} {operator} {right} = <output>{result}</output>
			</code>
		</div>
	)
}

const examples = (
	<>
		<Calculator left={1} operator="+" right={2} />
		<Calculator left={1} operator="-" right={2} />
		<Calculator left={1} operator="*" right={2} />
		<Calculator left={1} operator="/" right={2} />
	</>
)

Typing each operation function by hand is a minor annoyance, but it’s a worthwhile tradeoff for safer, self-maintaining types.