Here's our component without types:
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} />
</>
)
Right there you may notice we do things a little differently. Maybe you prefer this instead:
const Calculator = ({ left, operator, right }) => (
<div>
<code>
{left} {operator} {right} ={' '}
<output>{operations[operator](left, right)}</output>
</code>
</div>
)
I don't like the implicit return there. It means you can't reasonably declare variables or use hooks. So even for simple components, I never go with this approach.
Ok, so maybe you do this:
const Calculator = ({ left, operator, right }) => {
const result = operations[operator](left, right)
return (
<div>
<code>
{left} {operator} {right} = <output>{result}</output>
</code>
</div>
)
}
Honestly, that's fine most of the time. I personally like the hoisting characteristics of function declarations rather than function expressions like that (learn more).
Alright, let's add some types to this. For functions, you need to consider the types coming in and the types going out. Let's start with the input: props. To start, let's go with a simple type for the props (we'll improve it later):
type CalculatorProps = {
left: number
operator: string
right: number
}
With that, let's try some options for applying that type to the props object in our React Component.
A common method to typing a React component is to use one of the generics that
are built-into @types/react
(I mean, it's built-in right? So what could go
wrong?). Interestingly, you cannot type a function declaration this way, so
we'll have to use a function expression:
const Calculator: React.FC<CalculatorProps> = ({ left, right, operator }) => {
// implementation clipped for brevity
}
This works pretty well, but there are three major problems with this:
- Our
Calculator
function now accepts achildren
prop, even though we don't do anything with it 🙃 (So, this compiles:<Calculator left={1} operator="+" right={2}>What?</Calculator>
). - You can't use generics. Not super common, but definitely a downside.
- We have to use a function expression and can't use a function declaration.
Ok ok, so maybe #3 isn't a major problem, but #1 is pretty significant. There
are a few other smaller issues laid out in
this excellent GitHub issue
if you want to dive deeper (also check
the React TypeScript Cheatsheet).
Suffice it to say, don't use React.FC
(or its longer alias
React.FunctionComponent
).
One of the things I love about React components is that they aren't all that special. Here's the definition of a React component:
A React component is a function that returns something React can render.
Now, according to @types/react
, we're limited to null
and
React.ReactNode
s, but React can actually render strings, numbers, and booleans
as well. In any case, because a React component is simply a function that
returns something React can render, typing it can be just as straightforward as
typing functions. You don't have to do anything special just because it's React.
So here's how I'd type the props for this component:
function Calculator({ left, operator, right }: CalculatorProps) {
// implementation clipped for brevity
}
This doesn't have any of the shortcomings of React.FC
and it's no more
complicated than typing the arguments to a regular function.
Ok, so what about the return value? Well, we could type it as
React.ReactElement
or even wider as a React.ReactNode
. But honestly, I side
with my friend Nick McCurdy when
he says that
mistakes can easily be made causing the return type to be too wide. So even
outside a react context, I default to not specifying the return type (rely on
inference) unless necessary. And that's the case here.
Improving the CalculatorProps
type
Ok, now this next bit doesn't have a lot to do with typing React components, but
I thought you'd find it interesting anyway, so skip ahead if you don't. Let's
improve the CalculatorProps
type. As a reminder, here's what we have so far:
// 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>
)
}
I think the left
and right
types are fine. It's the operator
that I'm
unhappy with. Using string
is too wide. There are specific operations that are
allowed. For example, what would happen if we tried:
const element = <Calculator left={1} operator="wut" right={2} />
That right there is what we call a runtime exception my friends. That is...
unless you have strict
mode on, in which case you'd have a compilation error
on operations[operator]
. In strict mode, TypeScript will correctly know that
accessing any string from the operations
object will not necessarily return
a callable function.
There are plenty of ways to solve this problem. Basically, we want to limit the
operator
to only the supported operators. We can do that with a simple union
type:
type CalculatorProps = {
left: number
operator: '+' | '-' | '*' | '/'
right: number
}
But if we decided to add the
Exponentiation Operator
(**
), then we'd have to update not only the operations
object, but also the
operator
type which would be annoying. Maybe there's a way we can derive the
type for the operator
based on the operations
object? Why, yes there is!
type CalculatorProps = {
left: number
operator: keyof typeof operations
right: number
}
typeof operations
is going to get us a type that describes the operations
object, which is roughly equal to:
type operations = {
'+': (left: number, right: number) => number
'-': (left: number, right: number) => number
'*': (left: number, right: number) => number
'/': (left: number, right: number) => number
}
The keyof
part will take all the keys of that type, resulting in
'+' | '-' | '*' | '/'
🎉
Conclusion
Here's the finished version (I typed the operations 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: 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} />
</>
)
I hope that gives you an idea of a good way to type your React components. Good luck and take care!
P.S. One thing I don't like at all about our solution is we have to type each of
the operations
functions. Interestingly, this is a bit of a rabbit hole, but
at the other end of it, the types are definitely better and you learn a few
tricks along the way. Originally that was part of this blog post, but I decided
to move it to its own post. Read all about it here:
How to write a Constrained Identity Function (CIF) in TypeScript.