bg

TypeScript Features For React Developers


Posted on June 24, 2023
TypeScript
Tricks

Why TypeScript

As a React developer, you’re likely familiar with the benefits of using a statically-typed language like TypeScript. TypeScript offers a type system that helps catch errors early, making it easier to write more reliable code faster. In a React context, TypeScript can also provide additional benefits such as improved code maintainability and extensibility. If you’re new to TypeScript, it’s a superset of JavaScript that adds optional static types to your code. This means that you can catch errors before your code runs, which can save you time and improve the quality of your codebase.

ComponentProps utility type

A common pattern in React is to create a component that takes props as an argument. TypeScript provides a utility type called ComponentProps, which automatically extracts the props type from a given component. This saves you time and helps avoid errors when defining a component’s props.

test.jsx

_21
import React from 'react';
_21
_21
const MyComponent = ({ name, age }: { name: string; age: number}) => {
_21
return (
_21
<div>
_21
<p>{name}</p>
_21
<p>{age}</p>
_21
</div>
_21
);
_21
};
_21
_21
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
_21
_21
const App = () => {
_21
const props: MyComponentProps = {
_21
name: 'John',
_21
age: 26,
_21
};
_21
_21
return <MyComponent {...props} />;
_21
};

ComponentProps proves more useful when used with rest parameters and Native HTML Elements

test.jsx

_14
_14
import React from 'react';
_14
_14
function CustomButton(props: React.ComponentProps<'button'>) {
_14
return <button {...props}>Click me!</button>;
_14
}
_14
_14
function App() {
_14
return (
_14
<div>
_14
<CustomButton onClick={() => console.log('Button clicked!')} />
_14
</div>
_14
);
_14
}

In this example, we’re using the ComponentProps utility type to define the props for a custom CustomButton component that extends the functionality of the native button element. By passing the onClick prop to CustomButton, we're able to handle the button click event and log a message to the console. The ...props syntax allows us to pass any additional props to the underlying button element, such as className or disabled and if we tried to pass an invalid prop to the CustomButton element, Typescript will throw an error.

Union Types

Another useful TypeScript feature is the ability to use union types with useReducer. This allows you to define a state type that can have multiple variations. For example, you can specify the type of action and the exact payload key expected

test.jsx

_47
import { useReducer } from "react";
_47
_47
type State = { count: number };
_47
_47
type Action =
_47
| { type: "add"; add: number }
_47
| { type: "subtract"; subtract: number };
_47
_47
const reducer = (state: State, action: Action) => {
_47
switch (action.type) {
_47
case "add":
_47
return { count: state.count + action?.add };
_47
case "subtract":
_47
return { count: state.count - action?.subtract };
_47
default:
_47
throw new Error();
_47
}
_47
};
_47
_47
export const Reducer = () => {
_47
const [state, dispatch] = useReducer(reducer, { count: 0 });
_47
_47
function onAdd1() {
_47
dispatch({ type: "add", add: 1 });
_47
}
_47
_47
function onAdd2() {
_47
dispatch({ type: "add" }); // will give error because we didn't provide a payload (add) with a number value
_47
}
_47
function onSubtract1() {
_47
dispatch({ type: "SUBTRACT", subtract: 1 }); // will give error because type doesn't match
_47
}
_47
_47
function onSubtract2() {
_47
dispatch({ type: "subtract", subtract: "123" }); // will give error because payload subtract must be a number
_47
}
_47
_47
return (
_47
<>
_47
<h1>{state.count}</h1>
_47
<button onClick={onAdd1}>add</button>
_47
<button onClick={onAdd2}>add</button>
_47
<button onClick={onSubtract1}>subtract</button>
_47
<button onClick={onSubtract2}>subtract</button>
_47
</>
_47
);
_47
};

Excess Properties and Explicit Typing

Finally, TypeScript provides a way to strongly type the functions returned by useState. This helps ensure that you’re using the correct type when updating the state and provide better error handling.

When you use the useState hook in a React component with TypeScript, the type of the state is inferred based on the initial value you provide. However, when you update the state using setState, TypeScript does not perform excess property checking by default. This means that you can add additional properties to the object you pass to setState, even if they are not part of the type of the state.

Consider the code below:

test.jsx

_49
import { useState } from "react";
_49
_49
interface TagState {
_49
tagSelected: number | null;
_49
tags: { id: number; value: string }[];
_49
}
_49
_49
export const Tags = () => {
_49
const [state, setState] = useState<TagState>({
_49
tags: [],
_49
tagSelected: null,
_49
});
_49
return (
_49
<div>
_49
{state.tags.map((tag) => {
_49
return (
_49
<button
_49
key={tag.id}
_49
onClick={() => {
_49
setState((currentState) => ({
_49
...currentState,
_49
tagselected: tag.id,
_49
}));
_49
}}
_49
>
_49
{tag.value}
_49
</button>
_49
);
_49
})}
_49
<button
_49
onClick={() => {
_49
setState((currentState) => ({
_49
...currentState,
_49
tags: [
_49
...currentState.tags,
_49
{
_49
id: new Date().getTime(),
_49
value: "New",
_49
otherValue: "something",
_49
},
_49
],
_49
}));
_49
}}
_49
>
_49
Add Tag
_49
</button>
_49
</div>
_49
);
_49
};

In the example above, when the Add Tag button is clicked it adds a new tag to the tags array, with properties id, value, otherValue , if the user clicked on one of the tags, the tagselected will be the tag id, there are two issues with our code, can you spot them? we added otherValue to the tags array, this property didn’t exist in our original tags type, yet typescript didn’t spot the issue. the second issue is tagselected , our original TagState interface had a property called tagSelected camel-cased, typescript didn’t spot this issue as well.

To get an error for this bug we can use explicit typing:

example.jsx

_13
<button
_13
key={tag.id}
_13
onClick={() => {
_13
setState(
_13
(currentState): TagState => ({ //notice the change in this line
_13
...currentState,
_13
tagselected: tag.id, // will get an error due to explicit typing
_13
})
_13
);
_13
}}
_13
>
_13
{tag.value}
_13
</button>

example.jsx

_17
<button
_17
onClick={() => {
_17
setState(
_17
(currentState): TagState => ({ //notice the change in this line
_17
...currentState,
_17
tags: [
_17
...currentState.tags,
_17
{
_17
id: new Date().getTime(),
_17
value: "New",
_17
otherValue: "something", // will get an error due to explicit typing
_17
},
_17
],
_17
})
_17
);
_17
}}
_17
>

With explicit typing in place, TypeScript will now catch the typo in tagselected and the extra property otherValue, and throw an error. By catching these errors early, we can avoid bugs and improve the reliability of our code.

To summarize, TypeScript offers several useful features for React developers, including:

  • The ComponentProps utility type, which automatically extracts the props type from a given component and helps avoid errors when defining a component’s props.
  • Union types, which allow you to define a state type that can have multiple variations and provide better error handling with useReducer.
  • Explicit typing with useState functions, which helps ensure that you’re using the correct type when updating the state and catches errors early.

Explicit typing with useState functions, which helps ensure that you’re using the correct type when updating the state and catches errors early.

© Ari1009. All rights reserved.