DEV Community

bright inventions
bright inventions

Posted on • Edited on • Originally published at brightinventions.pl

5 (or more) ways to benefit from TypeScript in React projects

In the recent post we discussed how we can start a new React project with TypeScript to benefit from all the goodies of type checks and eliminate the whole class of errors and typos from our codebase. Now let’s see how we can apply TypeScript-based type definitions into our React project to get most of it.

To get started, as with every library we want to have the TypeScript support for, we need to grab the type definitions first. As usual, the way to do it is using the DefinitelyTyped project and run:

npm install --save-dev @types/react @types/react-dom
Enter fullscreen mode Exit fullscreen mode

The basics – Components

The components in React are in most cases ES6 classes that extend React-provided Component class. TypeScript is adding two generic arguments to this base class definition – the first one defines what our props are and the second is for the local component’s state.

interface HelloProps {
    greeting: string
}

interface HelloState {
    wasDisplayed: boolean
}

class HelloWorldComponent extends React.Component<HelloProps, HelloState> {}
Enter fullscreen mode Exit fullscreen mode

By adding these two definitions we’re not only adding a code completion capability to our component. Here I denoted I expect a greeting property and marked it as non-optional (no ? sign before a colon and no union type that allows undefined to sneak through). The compiler now successfully prevents us from using this component without the proper greeting property passed in.

Props completeness is enforced

Props & state (im)mutability

But there’s more. In React, both props and state are intended to be immutable. Props are for getting the data into the component only and state is to be modified via the specialized setState method. Thanks to both props and state defined as being of type Readonly<>, we are protected from accidentally mutating it:

Props immutability is enforced

As well as we’re not allowed to monkey-patch it, that is to add new properties there:

Props are frozen

Having said that, we must be aware that Readonly<> is not deep (recursive) – it only protects us from mutating the root level of the object, nothing protects its children objects:

interface State {
    inner: {stuff: string}
}

// this is still possible
this.state.inner.stuff = "inner’s properties are still mutable"
Enter fullscreen mode Exit fullscreen mode

Until React's typings implement the proper support for deep Readonly (which is possible starting from TypeScript 2.8), the best we can (and should) do is to ensure on our own that all the properties of both props and state of our components are also marked as Readonly<> (as well as its inner properties, ad infinitum):

interface State {
    inner: Readonly<{stuff: string}>
}

// now we’re safe again
this.state.inner.stuff = "this does not compile anymore"
Enter fullscreen mode Exit fullscreen mode

setState correctness

Another class of errors we’re automatically protected from is when we call setState with an invalid object. The first parameter of this function is defined with a rather cryptic type declaration:

state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
Enter fullscreen mode Exit fullscreen mode

But when reading piece by piece it tells us we either need to pass a function that returns Pick<S, K> | S | null or return it directly. And that Pick<S, K> | S | null thing is – reading backwards – either null, full state object itself (S) or an object with a subset of the state’s keys (Pick<S, K>). To cut the long story short, we are unable to pass the new state value object that doesn’t match our state definition. Here is the error that the TypeScript compiler gives us instead:

State correctness is enforced

Stateless components

Seasoned React developers are probably familiar with the simplest form of components available – stateless functional components. These are defined as pure functions that receive the props and return an JSX element. As simple as that, from the typing perspective it is usually sufficient to treat it like any other function – specifying types of its parameters and optionally the return type:

interface InputProps {
    value: any
    onChanged: () => void
}

function Input(props: InputProps) {
    return <input type="text" onChange={props.onChanged} value={props.value}/>
}
Enter fullscreen mode Exit fullscreen mode

We can have a problem here, though, if we want to specify propTypes or defaultProps for our stateless component. TypeScript will complain as plain functions do not have that kind of properties defined:

PropTypes cannot be added to plain function

We can solve it by declaring our component in a slightly different manner:

const Input: React.StatelessComponent<InputProps> = function (props) {
    return <input type="text" onChange={props.onChanged} value={props.value}/>
}

Input.propTypes = {
    value: PropTypes.any.isRequired
}
Enter fullscreen mode Exit fullscreen mode

But let’s stop and think for a moment what we’re trying to achieve here. Doesn’t TypeScript give us a similar level of type safety with much more convenience already? I find using PropTypes not needed anymore at all.

Events

Our next stop in the React world that might make use of stronger typing is around Events system. We take care of events every time we want our component to react to user actions. Let’s see our simplistic Input component once again:

interface InputProps {
    value: any
    onChanged: () => void
}

function Input(props: InputProps) {
    return <input type="text" onChange={props.onChanged} value={props.value}/>
}
Enter fullscreen mode Exit fullscreen mode

The onChange property, as every event handler, takes an event object as the only parameter. Let’s express it in our InputProps interface definition. Event is a name defined by the HTML spec itself, so let’s first try using it here:

onChanged: (event: Event) => void
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this seems to be not the event we should care about:

React does not use native HTML events

This rather verbose error gives us the expected type of an event, above anything else – see its last line. The event object passed by React is actually typed as ChangeEvent<HTMLInputElement> and this type seems not to extend the HTML built-in Event type. This is intentional because React doesn’t use the HTML events directly – it uses Synthetic Events wrappers instead.

When we change our Event type definition to the synthetic event type determined by both event type and element type, we are fine:

onChanged: (event: React.ChangeEvent<HTMLInputElement>) => void
Enter fullscreen mode Exit fullscreen mode

This gives us the best possible confidence level for what we can expect to get as an argument. It horribly reduces the flexibility, though. We can no longer have the same change handler for events fired on multiple types of HTML Elements (for example, <input> and <select>:

Events on different HTML elements are not compatible

We got an error indicating near the end that HTMLSelectElement is not assignable to HTMLInputElement. Well, it is not, indeed, and our handler was defined to accept the former only and we’re unable to reuse that handler directly. A similar problem occurs if we want to attach the same handler to the events of multiple types (i.e. change, click, mouse interaction etc.) – ChangeEvent<T> and MouseEvent<T> are not compatible.

Fortunately, TypeScript provides a decent set of type system features that might help us here. First, we can use the common ancestor event type instead of the specific ChangeEventSyntheticEvent is a good fit. The generic parameter that describes the type of element we handle our even on is more troublesome. We might try with a HTMLElement base type and in some cases it will suffice. But my usual case for the generic event handling is to handle multiple form elements and access its value attributes. Apparently there is no common type for all form elements that defines the value attribute. We have at least two ways to work around it. First, the mercy of union types where we can specify an alternative of types we want to handle and its common attributes will be freely available:

onGenericEvent: (event: React.SyntheticEvent<HTMLSelectElement | HTMLInputElement>) => void
Enter fullscreen mode Exit fullscreen mode

This is nice and explicit, although it doesn’t scale well if we want to handle more than few elements at once. The second solution uses the structural type compatibility – yet another extremely neat TypeScript’s type system functionality that allows us to define and compare types by its structure only. If our case is to read a value of value attribute of the handled element’s only, we might state it explicitly in our handler type definition:

onGenericEvent: (event: React.SyntheticEvent<{value: string}>) => void
Enter fullscreen mode Exit fullscreen mode

The TypeScript’s type system allows us to choose which level of specificity vs. flexibility is appropriate in our case.

Ugly case of generic setState

It’s not all flowers and roses, unfortunately. My typical scenario for form handling in React is to directly set the component’s state properties from the form elements values on their change events:

<input type="text" name="firstName" 
       onChange={event => this.setState({firstName: event.currentTarget.value})} />
Enter fullscreen mode Exit fullscreen mode

We might try to keep it generic by eliminating the duplication of the input’s name and assuming the name matches the key we want to set within the state and use enhanced object literals from ES6 specification (the thing with dynamic property names specified in square brackets):

<input type="text" name="firstName" 
       onChange={event => this.setState({[event.currentTarget.name]: event.currentTarget.value})} />
Enter fullscreen mode Exit fullscreen mode

As we have previously seen, TypeScript ensures the keys of the object we pass to setState match the actual properties of our component’s state. But here, TypeScript compiler (as of 2.6.1 at least) is not that smart to figure out what the actual value of event’s target name attribute would be, even though it can only be equal to firstName in this case. For TypeScript it is a general string and this is too wide to be considered valid for our setState call, unfortunately:

TypeScript is unable to specify down setState parameter types

We may work around it with type cast to inform TypeScript compiler what is the range of values we might potentially expect from event.currentTarget.name (assuming State describes the state of our component). The keyof State construct informs the compiler that the strings there may only be those that are defined by State interface structure:

<input type="text" name="firstName" 
       onChange={e => this.setState({[e.currentTarget.name as keyof State]: e.currentTarget.value})}/>
Enter fullscreen mode Exit fullscreen mode

Or, alternatively, if we want to avoid type casts, we may satisfy the setState call by ensuring the full state is always passed (including the expected modifications). It is actually taking advantage of a bit separate React’s feature than the partial state update, but should behave the same way:

<input type="text" name="firstName" 
       onChange={e => this.setState({...this.state, [e.currentTarget.name]: e.currentTarget.value})}/>
Enter fullscreen mode Exit fullscreen mode

Note I’m using not-yet-standard object spread operator here. It creates a copy of this.state and replaces (or adds) a single property to this copy – in this case it will set firstName property to the value of input’s value attribute, reflecting what the user has typed into the box.

What’s more?

As you might have already noticed, all the HTML elements have its attributes mapped into HTML*Element types we can benefit from whenever we’re operating on the elements. Similarly, a good subset of the CSS properties are mapped into the CSSProperties interface that defines all the predefined values the particular CSS property might use. This might be useful to use if we use any form of the inline styles in our components. It would provide a proper code completion and in some cases ensure the validation of our CSS definitions:

TypeScript helps with CSS validity

I hope you already feel that TypeScript can offer a lot of benefits to your React codebase, even though we’ve just touched the iceberg tip. In the next post we'll add Redux into the mix and see how TypeScript can help us there, too.

Originally published at brightinventions.pl

By Adam Bar, The Web Guy @ Bright Inventions
Personal blog Twitter Email Github

Top comments (1)

Collapse
 
nickytonline profile image
Nick Taylor

defaultProps support is coming in TypeScript 3.0 🔥👌