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
childrenprop 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.



