Deciphering TypeScript’s React errors
Static type checking is one of my favorite engineering guard rails, which is why I made adopting a typechecker a priority when choosing the toolset for webapp development on the City of Boston’s Digital team.
Static type checking de-risks software maintenance, since it reveals places where you need to update code to accommodate changes you’re making. It’s equally good for new development since, with good editor support, it ensures you’re calling functions that exist and are giving them the arguments they expect.
But, last Friday, I got a DM from my co-worker John, who has been writing some React components:
I hate those errors, I was going to say dislike but naaaaahhh I hate them lol
John’s a recent bootcamp grad, working with me for the summer as a fellow. He’s also super-chill, so seeing this strong reaction really woke me up.
I had thrown John into a type-checking world without preparation. He fell victim to a paradox of guard rails: it can take more effort to deal with a checker’s “helpful” error messages than it would ever be to debug the problems it’s warning you against.
The combination of React, JSX, and the DOM make for some hairy TypeScript types. When TypeScript complains, its error messages are verbose, with a lot of names you don’t recognize from your own code. So, for John and everyone else new to this environment, I present my guide to decoding your React / TypeScript error messages.
(You might notice that the Registry webapp was written with Flow. For new development we’re working in TypeScript.)
Why even typecheck React?
Let’s see what we hope to get out of using TypeScript with React in the first place. When things go smoothly, we’ll be catching these bugs:
- Trying to pass a prop that a component doesn’t want
- Forgetting to pass a prop that a component requires
- Getting a prop’s type wrong, such as passing a string when it expects a number
If we write these bugs, TypeScript will show an error in our editor right away. Without TypeScript, we’d have to catch these bugs later during testing, and it might be tedious debugging to figure out where they come from.
(Whether or not it’s worth it to use a tool to catch these bugs is a question for your team. I find it valuable, which is why we’re here.)
Errors with DOM elements
For starters, let’s look at using standard DOM elements in JSX. TypeScript will check to make sure that every attribute you put on an HTML tag exists and is of the right type. For example:
render() {
return <input type="text" name="color" />;
}
The above React/JSX code type checks fine, because <input>
s have both type
and name
attributes! You won’t see any errors. TypeScript can do this because there’s a React library (@types/react
) that defines all HTML elements and what attributes they each take.
Note: In this guide I use “attribute” and “property” pretty much interchangeably. “Attribute” is what they’re called in HTML, but on React components they’re “props.” And, when they’re on a JavaScript Object, TypeScript calls them “properties.”
Using a property that doesn’t exist
Now let’s mis-spell “name
” and see what happens:
render() {
return <input type="text" nmae="color" />;
}
If you have TypeScript support in your editor, you should get a red underline on that line, and the following error message:
That looks a little daunting, but we can break it down piece-by-piece:
Property 'nmae'
— The “properties” are the attributes on the<input>
tag. So this error is about “nmae=”…"
”.does not exist on type
— There’s a “type,” a definition somewhere of what is allowed to be on<input>
, and “nmae
” isn’t on the list.'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'
— OK, this is a doozy. This describes how TypeScript thinks of<input>
’s attributes. All of these names (DetailedHTMLProps
,InputHTMLAttributes
, andHTMLInputElement
) are defined in that React library.DetailedHTMLProps
means attributes that any HTML element can have (likeid
,tabIndex
, andstyle
) andInputHTMLAttributes
are for ones specific to<input>
elements (name
,value
, &c.). This is boilerplate you can typically gloss over, but theHTML***Element
part can be a useful clue in deciphering this.
The fix: When you see “Property XXX does not exist on type
,” it means you’re adding an XXX="…"
to an element that doesn’t want an XXX
on it. While that is not itself a problem—the element would just ignore attributes it doesn’t recognize—it’s usually a sign of a bug. In this case, it’s not a problem to add a nmae
, but it is a problem that we thought we were adding a name
but didn’t actually.
When you see this, check for typos, and check the HTML docs to make sure you’ve got the attribute or element right for what you’re trying to do.
Getting a property’s type wrong
There’s another error that can come up with HTML: using the wrong type for an attribute. Let’s add a size
to the <input>
and see what happens:
render() {
return <input type="text" name="color" size="6" />;
}
This is exactly how you’d write it in an HTML page, but TypeScript shows an error:
Types of property 'size' are incompatible.
— We’re talking aboutsize
now. Note how it says “types” with a plural. That’s because all type errors are inconsistencies. This error is between two types: the one from our code, and the one from React’s<input>
definition (remember theDetailedHTMLProps<…>
bit we saw in the last error).Type 'string'
— This is the first of the types, the one for the argument that we wrote."6"
is a string, because all quoted JSX attributes are strings.is not assignable to
— This is a nuanced way of saying “isn’t.” It’s clarifying what is “incompatible” about the two types.type 'number | undefined'
— This is the second of the two types, the one that the React library said is right for<input>
’ssize
attribute. The|
is known as a “union” operator, but you can read it as “or”: “size
must be a number or undefined.”
The fix: We need to change one or the other of the types to make them consistent. Since we can’t change the React library to accept a string, we have to change our code to pass a number:
render() {
<input type="text" name="color" size={6} />
}
Remember that {}
is the way to make a JavaScript expression in JSX. Now instead of a string, we’re providing a number, and the TypeScript error goes away.
The error message said that the type from the React library was 'number | undefined'
. “Undefined” is the same as leaving it off, which is why our previous examples typechecked without a size
. But this explains why the error message said “is not assignable to” instead of “isn’t.” 6
is a number, not a “number or undefined.” But, a number is assignable to a “number or undefined,” because of the “or.”
The React type library doesn’t require any attributes on HTML elements, so there’s no “forgetting to pass a prop that a component requires” error in this section. That will come up when we deal with custom components.
All type errors are inconsistencies
I mentioned this above in the breakdown of the size
bug, but it’s an important enough concept that I want to call it out:
All type errors are inconsistencies.
What I mean by this is those red TypeScript squiggles are there because one part of your code doesn’t agree with another part. Which part is right? Your job is to figure that out by thinking about what you’re trying to make your code do.
One of the reasons that type errors can be confusing to interpret is that TypeScript will only underline one of the places where the inconsistency is happening. The first example below is an error where TypeScript calls out something in the JSX, but that part is actually right and it’s the type definition that we decide is wrong.
Errors with custom React components
The principles of typechecking custom components are basically the same as DOM elements, but the names of the types get more confusing.
For this section, we’ll use the following sample component:
class CustomInput extends React.Component {
render() {
return <input />;
}
}
Using your own props
Say we’re using this to build a form, so we need to pass in a name
that can go on that <input>
. In React, that means adding a prop. Here’s the new render
method:
render() {
return <input name={this.props.fieldName} />
}
Which might get used by another component like this:
render() {
return (
<form>
<CustomInput fieldName="colorField" />
</form>
);
}
Seems straightforward, and in plain JavaScript this case would totally work, no bugs. But TypeScript has found something to complain about:
Property 'fieldName'
— We saw this language before with the<input>
’snmae
attribute, but now it’s referring to a “property” on thethis.props
object, rather than one on an HTML tag.does not exist on type
— This is also the same language. TypeScript has an opinion about what properties are and are not onthis.props
, andfieldName
is in the “not” pile.'Readonly<{ children?: ReactNode; }> & Readonly<{}>'
— OK, this is hard to read. This is the type that TypeScript thinks of when we saythis.props
.
That last bit needs a more detailed explanation:
- The
Readonly
s mean that you can’t changethis.props
. You shouldn’t be doing that with React, so TypeScript enforces it. { children?: ReactNode; }
is there because all React components can have athis.props.children
value.ReactNode
is anything that React knows how to render: strings, components, and so on. The?
means that this property is optional—you don’t need to pass children toCustomInput
.&
in TypeScript makes an “intersection” between two types, which is kind of like an object that has all the properties from each of them merged together. So,this.props
has a type that’s made up of all the properties of{ children?: ReactNode; }
combined with all the properties of{}
. Which doesn’t have any.
That comes together to mean that this.props
has an optional children
property, but no fieldName
property.
The fix: To fix this, we actually have to make a choice. Say it with me now:
All type errors are inconsistencies.
Two things don’t agree. In your render
method you’re trying to use this.props.fieldName
. But when you declared your component, you didn’t give it a fieldName
prop. Which is right? The render
method or the component declaration?
With DOM elements, we knew that the expected attributes were correct because they came from a library we trust. But this is our own code, so it’s up to us to pick what to change to make the error go away. That means either stop trying to use this.props.fieldName
, or add fieldName
to our component’s declaration.
By default, all React.Component
s have a this.props
type of {}
, which we saw in the &
statement in the error. Since we really do want to have a fieldName
prop, we have to add it:
interface CustomInputProps {
fieldName: string;
}class CustomInput extends React.Component<CustomInputProps> {
render() {
return <input name={this.props.fieldName} />;
}
}
What’s going on here? We start with interface
, which is how we make a new “object type” in TypeScript. We’re saying that any object of type CustomInputProps
must be “an object with a fieldName
property, which must be a string.”
Aside: Why are we talking about object types?
CustomInputProps
is an object type, which means that it describes the “shape” of a JavaScript object. For example:
const obj: CustomInputProps = {
fieldName: 'myInput',
};
That’s a const
called obj
, and : CustomInputProps
tells TypeScript that its value needs to match the CustomInputProps
shape. We initialize it with an object literal that has the necessary fieldName
property, so it typechecks!
Let’s leave off the fieldName
property and see what happens:
const obj: CustomInputProps = {};
{}
doesn’t match CustomInputProps
, so here’s how TypeScript complains:
Type '{}'
— This is TypeScript describing the type of the value that we’re trying to assign toobj
: an object with no fields.is not assignable to
— Our old friend “is not assignable to.” We’re trying to make an assignment: assigning{}
toobj
, but TypeScript is saying we can’t do it.type 'CustomInputProps'.
— This is here because it’s what we declaredobj
to be. Therefore it’s the type that’s being assigned to.Property 'fieldName' is missing in type '{}'.
— Now TypeScript is being specific about why an object of type{}
is not assignable to something of typeCustomInputProps
: There’s nofieldName
, and, because of theinterface
declaration from above, allCustomInputProps
objects must have afieldName
.
Now let’s see what happens when we put back fieldName
, but also add a field that’s not part of the CustomInputProps
type:
const obj: CustomInputProps = {
fieldName: 'myInput',
placeholder: 'Type something here',
};
That’s not allowed:
Type '{ fieldName: string; placeholder: string; }'
— This is the new type of our object literal. See how it now has theplaceholder
property.is not assignable to type 'CustomInputProps'.
— Same as before. TypeScript thinks this assignment is illegal.Object literal may only specify known properties, and 'placeholder' does not exist in type 'CustomInputProps'.
— This is the explanation for why this case is “not assignable.”CustomInputProps
doesn’t have aplaceholder
property declared in itsinterface
block.
This is a similar case to when we put nmae
on an <input>
. Adding the extra placeholder
property is not in itself a bug, since code working on CustomInputProps
would ignore it, but it’s a sign that there’s probably a bug.
Back to why we’re even talking about object types in the first place. What do they have to do with React props? Objects have {}
squiggly braces, not the <>
angle brackets of JSX. To explain this, let’s go back to our DOM element example and talk a bit about JSX.
<input name="color" size={6} />
Browsers can’t understand this JSX on their own, they only understand plain JavaScript. We typically use the TypeScript compiler or a tool like Babel to take the JSX and make JavaScript out of it. Here’s what that code looks like as JavaScript:
React.createElement('input', {
name: 'color',
size: 6,
});
Hey, those name="color"
and size={6}
attributes are an object literal now! The first argument to createElement
is what we’re creating—a class for custom components, a tag name for HTML elements—and the second argument are the props to pass to it.
This is why TypeScript is always talking about props as object types. When it type checks the React code, it makes sure that that second argument to createElement
has a type that matches (“is assignable to”) the declared type of the component’s props.
(Because it just transforms things into plain JavaScript, JSX is sometimes referred to as “syntactic sugar.” It’s not actually necessary—we could write those createElement
statements ourselves—but it makes the code sweeter.)
Now back to declaring props types
Let’s remember where we were:
interface CustomInputProps {
fieldName: string;
}class CustomInput extends React.Component<CustomInputProps> {
render() {
return <input name={this.props.fieldName} />;
}
}
Now that we’ve declared the interface
we want—the “shape” of our component’s props—we need to actually tell TypeScript to use it for CustomInput
. That’s where the theReact.Component<CustomInputProps>
comes in. The <…>
bits let us pass a type parameter. Think of it as a way of tweaking React.Component
, kind of like how you use (…)
s to pass arguments to functions. ReactComponent
uses its first type parameter as the type for this.props
, replacing the default {}
.
We can go back to our render
method and see what this.props
is now. In Visual Studio Code, you can hover over it:
React.Component<CustomInputProps, {}, any>
— This is Visual Studio telling us whatthis
is. It closely matches theextends React.Component<CustomInputProps>
from our class declaration, just with the default values forCustomInputProps
’s 2 additional type parameters written out explicitly..props:
— Because that’s what we’re hovering over.Readonly<{ children?: React.ReactNode; }> & Readonly<CustomInputProps>
— Remember thisReadonly
stuff from before? Now it hasCustomInputProps
instead of{}
.
Cool! With this change, this.props
keeps the previous children
property, but now also has everything from CustomInputProps
, in particular the fieldName
property.
There’s also state
Using this.state
is nearly identical to this.props
, so I won’t go into it in depth. The errors you get from it should be basically the same. To give a component’s state a type, use the second type parameter to React.Component
, like so:
interface Props {
fieldName: string;
}interface State {
error: string | null;
}class CustomInput extends React.Component<Props, State> {
state = {
error: null;
};
}
The above example also shows initializing state
with a default value. You could also do this with this.state = {…}
in the constructor()
method. I’ve also shortened the interface names to “Props
” and “State
” so they don’t wrap. They can actually be anything, as long as you’re consistent between the interface
and the React.Component<…>
.
Required props on a custom component
Let’s go back to using our CustomInput
component. This will be similar to when we were making <input>
elements.
Remember where we left off with CustomInput
:
interface CustomInputProps {
fieldName: string;
}class CustomInput extends React.Component<CustomInputProps> {
…
}
Let’s make one!
render() {
return (
<form>
<CustomInput />
</form>
);
}
And the error is…
It just keeps getting more complicated, doesn’t it?
Type '{}'
— To explain this, remember how JSX works. Our above line of code turns intoReact.createElement(CustomInput, {})
.{}
because we didn’t write any props in the JSX.Type '{}'
is TypeScript describing that second, props argument.is not assignable to
— The old standby. There’s an inconsistency in the types!'IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }>...'
— This is such a mess that TypeScript literally gives up and stops before it gets to the end. The “intrinsic” bits cover common React props likekey
andref
. You can see theReadonly<{ children?: ReactNode; }>
at the end, which is the start of thethis.props
type we saw before. The complete type here is:IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }> & Readonly<CustomInputProps>
, but honestly you can glaze over this line.Type '{}' is not assignable to type 'Readonly<CustomInputProps>'
— Here TypeScript calls out what part of theIntrinsicAttributes & … & …
it’s having trouble with.{}
is perfectly assignable toIntrinsicAttributes
,IntrinsicClassAttributes
, andReadonly<{ children?: ReactNode; }>
because all of their fields are optional. What it’s not assignable to is ourCustomInputProps
.Property 'fieldName' is missing in type '{}'.
— This is the most useful part of the error message. Thetype '{}'
is calling back to that secondcreateElement
argument, the type of the props from the JSX.
The fix: Once again, type errors are flagging inconsistencies in the code. As written, CustomInput
wants a fieldName
prop, but you’re not giving it one. So, either give it one, or make it not want one in the first place. Let’s look at how we would do each.
To pass fieldName
, we do this:
<CustomInput fieldName="myField" />
After the JSX translation, that turns into:
React.createElement(CustomInput, { fieldName: "myField" });
The type of that second, props argument is { fieldName: string }
which is assignable to CustomInputProps
.
Here’s the other case, making fieldName
optional. For this, we need to change the interface
where CustomInputProps
was declared.
interface CustomInputProps {
fieldName?: string;
}
See how there’s a ?
there now? That tells TypeScript that this property is optional. Now both { fieldName: string }
and {}
are assignable to CustomInputProps
.
This does mean that this.props.fieldName
now has the type 'string | undefined'
, so TypeScript will make you take that into account in the rest of your code. No calling this.props.fieldName.length
without checking that it’s defined first!
Passing unknown props
Finally, let’s look at the error messages you get when you give a prop to a custom component that isn’t in its props type. TypeScript has two different error messages, depending on what other props you pass.
<CustomInput fieldName="myField" placeholder="Type here" />
This error message is the same one we got with nmae
in the early example, just with CustomInput
’s longer props type at the end. It is very unfortunate that this is cut off before it says CustomInputProps
, because that’s a piece of information that would really help track down what’s going on. Just remember that when you see this, it’s most likely that the prop you’re passing it isn’t defined in the component’s own props interface.
Curiously, if we leave off fieldName
, the error message is different:
export interface CustomInputProps {
fieldName?: string;
}<CustomInput placeholder="Type here" />;
We can see the { placeholder: string; }
type from the createElement
statement that’s behind the scenes, and the IntrinsicAttributes & …
type for CustomInput
’s props. It’s just for a reason I don’t know TypeScript says “has no properties in common” rather than “does not exist.”
The fix: Resolve the inconsistency! Either add placeholder
to CustomInputProps
or remove it from <CustomInput … />
.
Whew!
There we go. Explanations for the most common TypeScript errors you run into in your React JSX, and how you might solve them. Hopefully that will reduce any future rage when programming.
I will admit that finding new ways for type checking to catch errors is one of my favorite programming exercises. In the new Boards and Commissions codebase, we’re using ts2gql
to turn TypeScript interfaces into a GraphQL schema, which means we can use those TypeScript interfaces to very precisely typecheck our resolvers. Just as with the Registry webapp, we’re using apollo-codegen
to generate argument and output types for GraphQL queries, and we’ve also added sql-ts
to make TypeScript types from SQL Server tables.
It’s a good reminder to me, and anyone else who’s putting tooling together, that a guard rail is only as good as whether or not you can tell what it’s guarding against.
Acknowledgements
Guillaume Marceau, way back in his grad school days, helped me understand how type errors arise from two conflicting parts of a program. James Duffy proofread this article.
And of course to John, for powering through all of this like a champ.