Deciphering TypeScript’s React errors

Fiona Hopkins
Jun 28, 2018 · 16 min read

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:

“Property ‘nmae’ does not exist on type ‘DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>’”

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, and HTMLInputElement) are defined in that React library. DetailedHTMLProps means attributes that any HTML element can have (like id, tabIndex, and style) and InputHTMLAttributes are for ones specific to <input> elements ( name, value, &c.). This is boilerplate you can typically gloss over, but the HTML***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. Type ‘string’ is not assignable to type ‘number | undefined’.
  • Types of property 'size' are incompatible. — We’re talking about size 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>’s size 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’ does not exist on type ‘Readonly<{ children?: ReactNode; }> & Readonly<{}>’.
  • Property 'fieldName'— We saw this language before with the <input>’s nmae attribute, but now it’s referring to a “property” on the this.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 on this.props, and fieldName 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 say this.props.

That last bit needs a more detailed explanation:

  • The Readonlys mean that you can’t change this.props. You shouldn’t be doing that with React, so TypeScript enforces it.
  • { children?: ReactNode; } is there because all React components can have a this.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 to CustomInput.
  • & 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.Components 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 ‘{}’ is not assignable to type ‘CustomInputProps’. Property ‘fieldName’ is missing in type ‘{}’.
  • Type '{}'— This is TypeScript describing the type of the value that we’re trying to assign to obj: an object with no fields.
  • is not assignable to— Our old friend “is not assignable to.” We’re trying to make an assignment: assigning {} to obj, but TypeScript is saying we can’t do it.
  • type 'CustomInputProps'.— This is here because it’s what we declared obj 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 type CustomInputProps: There’s no fieldName, and, because of the interface declaration from above, all CustomInputProps objects must have a fieldName.

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; }’ is not assignable to type ‘CustomInputProps’. Object literal may only specify known properties, and ‘placeholder’ does not exist in type ‘CustomInputProps’.
  • Type '{ fieldName: string; placeholder: string; }'— This is the new type of our object literal. See how it now has the placeholder 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 a placeholder property declared in its interface 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>.props: Readonly<{ children?: React.ReactNode; }> & Readonly<CustomInputProps>
  • React.Component<CustomInputProps, {}, any>— This is Visual Studio telling us what this is. It closely matches the extends React.Component<CustomInputProps> from our class declaration, just with the default values for CustomInputProps’s 2 additional type parameters written out explicitly.
  • .props:— Because that’s what we’re hovering over.
  • Readonly<{ children?: React.ReactNode; }> & Readonly<CustomInputProps>— Remember this Readonly stuff from before? Now it has CustomInputProps 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…

Type ‘{}’ is not assignable to type ‘IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }>…’. Type ‘{}’ is not assignable to type ‘Readonly<CustomInputProps>’. Property ‘fieldName’ is missing in type ‘{}’.

It just keeps getting more complicated, doesn’t it?

  • Type '{}'— To explain this, remember how JSX works. Our above line of code turns into React.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 like key and ref. You can see the Readonly<{ children?: ReactNode; }> at the end, which is the start of the this.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 the IntrinsicAttributes & … & … it’s having trouble with. {} is perfectly assignable to IntrinsicAttributes, IntrinsicClassAttributes, and Readonly<{ children?: ReactNode; }> because all of their fields are optional. What it’s not assignable to is our CustomInputProps.
  • Property 'fieldName' is missing in type '{}'.— This is the most useful part of the error message. The type '{}' is calling back to that second createElement 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" />
Property ‘placeholder’ does not exist on type ‘IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }>…’.

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" />;
Type ‘{ placeholder: string; }’ has no properties in common with type ‘IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }>…’.

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.

Innovation and Technology

The City of Boston’s Innovation and Technology Department…

Thanks to City of Boston

Fiona Hopkins

Written by

Software engineer. I’m into board games, web development, and social justice. https://fionawh.im/ (she/her)

Innovation and Technology

The City of Boston’s Innovation and Technology Department engages, empowers, and improves life for residents in the City through technology.

More From Medium

Related reads

Related reads

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade