Search justacoding.blog. [Enter] to search. Click anywhere to close.

October 18th, 2021

React With TypeScript: Typing Components

This article is focussed on using React with TypeScript. More specifically, we’ll be looking at typing React components in general: their state, their props, their return types and so on.

So if you’re new to using React with TypeScript, or if you just need a refresher — this short guide should (hopefully) be of some benefit.

And with that, let’s begin!

Declaring props and their types

There are multiple ways to go about declaring the type of your component’s individual props.

The recommended and most common approach is to define the props in their own interface (or their own type). Outside of that, you can declare them “inline” – so the type of each prop would be specified directly alongside where we pass the props to the component.

Let’s take a look at both approaches in turn.

Create an interface (or type) for your component’s props

This is the recommended approach.

Here, we’re using an interface to declare each individual prop along with it’s relevant type (more on the possible types we can use later on):

interface ProfileProps {
    name: string
    age: number
    address?: string
}

const Profile = ({ name, age, address }: ProfileProps) => {
    return {
        <>
            <h1>Welcome back, {name}!</h1>
            <p>Age: {age}</p>
            <p>Address: {address ? address : "Not set"}
        </>
    }
})

This is the preferred approach over the inline declaration below because you can easily export an interface for use elsewhere in the app.

It’s also cleaner and syntactically easier to read.

Declare your component’s prop types inline

And here’s the inline approach:

const Profile = ({ name, age, address }: 
    { name: string, age: number: address?: string }) => {
    // ...
})

The single benefit here is that we don’t need to declare the props (by creating a new interface or type) elsewhere beforehand. So we can consider this approach to be a little bit more concise and less repetitive in this regard.

However, it’s a bit visually cumbersome and in my opinion it’s not as clear or as easy to read.

Unlike with the previous interface example — it also means we can’t easily export the prop type declarations for other components to use should they required them.

Declaring the props and state types for class components

The example above is demonstrated using a function component, but we can do more or less the same thing for class components.

However, the main difference is that you should also (additionally) declare what your component’s state should look like, too. We don’t need that approach with the function component, because the component’s internal state is handled or managed with the useState hooks.

Here’s an example of declaring types for our class component’s props and it’s state when using React with TypeScript:

interface ProfileProps {
    name: string
    age: number
    address?: string
}

interface ProfileState {
    someState: string
    moreState: boolean
}

class Profile extends React.Component<ProfileProps, ProfileState> {
    state: ProfileState = {
        someState: "This is a piece of state",
        moreState: 44
    }

    render() {
        return (
            // ...
        )
    }
}

With TypeScript, React.Component has been enhanced to use generics — so we can pass in the props and state interfaces as demonstrated above.

What types can I use?

When using React with TypeScript, it’s not always easy or intuitive to identify the correct or most appropriate type your variable should be using in each and every case. There are cases where there are multiple possible types you can use for your declaration, and it can be hard to reason about each!

I’ve tried to cover those cases in the next section, and subsequently, later on in the article.

I’ve also listed out some commonly used TypeScript types that you’ll likely be using at some point or another when declaring the props for your React components.

The basic types

These are the most basic TypeScript types.

We can have an array variant of each type by prepending the square brackets ([]) as shown.

Additionally, we can declare “optional” types using ? during type declaration. This means that the given property isn’t specifically required to be set or assigned by members that implement this interface.

interface ProfileProps {
    name: string
    age: number
    isValid: boolean
    // array of strings
    references: string[]
    // an optional string
    address?: string
}

String literals

String literals in TypeScript are handy when your variable should only be one of a number of different strings.

interface ProfileProps {
    registrationStatus: "COMPLETE" | "IN PROGRESS" | "CANCELLED"
}

Of course, in most cases we could just use a regular string type, too. But the string literal approach is clearly much safer and much more robust, as we’re limiting or restricting the possible values based on the values we’ve specifically indicated.

Objects

With objects, there are a few different ways we can apply our typings.

interface ProfileProps {
    // any object...
    verificationData: object
    // similar, any object
    moreVerificationData: {}
    // specific object, use this!
    anotherVerificationDataObject: {
        id: number
        title: string
        isValid: boolean
    }
    // an array of specific objects
    multipleVerificationDataObjects: {
        id: number
        title: string
        isValid: boolean
    }[]
}

As you may have correctly assumed, it’s much better to define the properties within the object. We can then benefit fully from using TypeScript in this regard.

When using React with TypeScript, or even just TypeScript in general — you should ensure that you’re letting TypeScript do it’s job properly! So always be as explicit as possible with your type declarations.

Using {} or object can be handy as a placeholder, but it means these objects can have any properties. So they become harder to work with, and the implementation becomes less robust with this approach.

You simply cannot/do not benefit from TypeScript as it can’t predict or determine what properties may or may not exist within these object(s).

So in short, you’ll lose some power (and type safety) by not defining the object properties explicitly each time.

Functions

There are also multiple ways to type out your functions.

interface ProfileProps {
    // any function at all...
    update: Function
    // functions, but with the arguments/return types defined
    anotherUpdate: () => void
    yetAnotherUpdate: (id: number, profileData: object) => boolean
    onClick(event: React.MouseEvent<HTMLButtonElement>): void
}

As with when typing objects, it’s generally much better to be as explicit or specific as possible.

The Function type expects any function.

It’s fairly likely that this declaration can be more specific, though, so it’s better to be more restrictive if possible.

You can do this by defining:

  • what arguments should be passed to the function
  • what the return type of the function is

Again, this leads to better type safety, and TypeScript will do it’s job more effectively in ensuring your code is more robust and error-free.

Children/child components

When using React with TypeScript, you’ll often need to define the types for child components. There are multiple ways in which this can be handled.

Each approach has it’s own pros and cons, which we’ll discuss later. For now, here’s a demonstration of some of the types you can expect to encounter in this scenario.

interface PropsProps {
    child1: JSX.Element
    child2: JSX.Element[]
    child3: React.Children
    child4: React.Child[]
    child: React.ReactNode
    childFunction: () => React.ReactNode
    style: React.CSSProperties
    onChange?: React.FormEventHandler<HTMLInputElement>
}

Typing hooks

Typing the useState hook

When typing the useState hook, it’s often the case that you won’t need to explicitly declare types. This is particularly the case with simple types, like booleans, or strings:

const [isValid, setIsValid] = useState(false)

Here, type inference is used and it fits our need perfectly.

isValid is inferred to be a boolean (which we want), and our function — setIsValid will only accept boolean values, too.

So that all works as expected, without the need for explicit type declarations.

useState with custom types

When using custom types with the useState hook, it’s simply a case of explicitly declaring the type, like so:

const [profile, updateProfile] = useState<Profile>({} as Profile)

The one caveat here is to do with {} as Profile.

The Profile type may well have one (or more) required properties, and the rest of your code may rely on properties types being set when dealing with the given Profile object. For instance id or name — your implementation may rely on those properties existing on the Profile object.

As we are effectively assigning an empty object here, that could potentially be problematic. So it’s something to bear in mind when declaring types in this way!

useState if your value could be null

If it’s possible for your value to be null (or undefined), as is commonly the case, you can use what’s called a union type.

That would look something like this:

const [profile, updateProfile] = useState<Profile | null>(null)

So as you can see, the value of profile is initially null. It’ll subsequently be of the Profile type instead once updateProfile has been invoked with the required profile object.

Using default props

It’s worth nothing that you can still apply default prop values whilst using the React interface upon which your props are based:

const Profile = ({ name = "Tom", age = 33, address = "" }: ProfileProps) => {
    // ...
})

This is clearly very useful, as we don’t need to handle this later on (within the component body) with if/else statements or something similar.

It’s just a neat and concise way to ensure our props default as we would want them to in any given scenario.

What is the return type of my React component?

Of course, you can (and possibly should) define the return type of your components when using React with TypeScript.

This is typically a good idea, as it’ll prevent accidental returns of an unexpected type from within your component. You’ve likely often wondered “what should my component return?” – and there are a few potential options.

const Profile = ({ name: string }): JSX.Element => {
    // ...
})

Generally speaking, you’ll probably want to return either a React.ReactNode or a JSX.Element. These two are fairly similar, but there are some important differences.

To briefly summarize what these two types represent:

  • React.ReactNode basically indicates the return type (any return type) of a component. So it is not particularly specific in this regard, as that return type can be any number of things.
  • JSX.Element is a bit more restrictive, it always returns an object and not all of the possible return types that are captured by React.ReactElement.

You’re using interfaces, shouldn’t you be using types?

Within TypeScript, it’s perhaps easy at first to confuse type with interface. The two are fairly similar, conceptually, however — there are some key differences.

It’s worth noting that in the examples in this article, it’s possible to use a type or an interface. Both will work equally well in terms of declaring state and prop types for our components.

However, with that in mind, you should be aware of how these two concepts do actually differ.

I’d recommend reading more about the intricacies of this particular debate/discussion. This article from LogRocket is particularly illuminating read.

Thanks for reading!

Have any questions? Ping me over at @justacodingblog
← Back to blog