DEV Community

Cover image for 10 Must-Know Patterns for Writing Clean Code with React and TypeScript✨🛀
Alex Omeyer
Alex Omeyer

Posted on • Updated on

10 Must-Know Patterns for Writing Clean Code with React and TypeScript✨🛀

React is a JavaScript library, and it is the most popular and industry-leading frontend development library today.

JavaScript is a loosely typed language, and as a result, it catches runtime. The result of this is that JavaScript errors are caught very late and this can lead to nasty bugs. As a JavaScript library, React inherits this problem.

Clean code is a consistent style of programming that makes your code easier to write, read, and maintain. Anyone can write code that a computer can understand but good developers write clean code – code that humans can understand.

Clean code is a reader-focused development style that improves our software quality and maintainability.

Writing clean code involves writing codes with clear and simple design patterns that makes it easy for humans to read, test and maintain. Consequently, clean code can lower the cost of software development. And this is because the principles involved in writing clean code, eliminates technical debts.

In this article, we would look at some useful patterns to use when working with React and TypeScript.

đź’ˇ To make it easier for your team to keep codebase healthy and prioritise technical debt work, try out the Stepsize extension. It helps Engineers create technical issues, add them to the sprint, and address tech debt continuously - without leaving the editor.

Now let’s learn about the ten useful patterns to apply when using React and Typescript:

1. Use Default import to import React

Consider the code below:

While the code above works, it is confusing and not a good practice to import all the contents of React if we are not using them. A better pattern is to use default export as seen below:

With this approach, we can destructure what we need from the react module instead of importing all the contents.

Note: To use this option, we need to configure the tsconfig.json file as seen below:

In the code above, by setting esModuleInterop to true we enable [allowSyntheticDefaultImports](http://allowsyntheticdefaultimports) which is important for TypeScript to support our syntax.

2. Declare types before runtime implementation

Consider the code below:

The code above can be cleaner and more readable if we separate the runtime and compile-time declarations. And this is done by declaring the types — the compile type declarations first.

Consider the code below:

Now at first glance, a developer knows what the component API looks like since the first line of the code clearly shows this.

Also, we have separated our compile-time declarations from our runtime declarations.

3. Always provide explicit type of children Props

TypeScript mirrors how React handles children props by annotating it as optional in the react.d.ts for both functional and class components. Consequently, we are required to explicitly provide a type for the children props. However, it is best practice to always explicitly annotate children props with a type. This is useful in cases where we want to use children for content projection, and if our component does not use it, we can simply annotate it with the never type.

Consider the code below:

Below are some valid types to annotate the children props:

  • ReactNode | ReactChild | ReactElement
  • For primitive we can use string | number | boolean
  • Object and Arrays are also valid types
  • never | null | undefined – Note: null and undefined are not recommended

4. Use type inference for defining a component state or DefaultProps

Consider the code below:

While the code above works we can refactor it for the following improvements:
To enable TypeScript’s type system to correctly infer readonly types such as DefaultProps and initialState
To prevent developer bugs arising from accidentally setting state: this.state = {}
Consider the code below:

In the code above, by freezing the DefaultProps and initialState the TypeScript type system can now infer them as readonly types.

Also, by marking both static defaultProps and state as readonly within the class we eliminate the possibility of runtime errors arising from setting state as mentioned above.

5. Use type alias instead of interface for declaring Props/State

While interface can be used, for consistency and clearness sake it is best to use type alias as there are cases where interface cannot work. For instance, in the previous example, we refactored our code to enable TypeScript’s type system to correctly infer readonly types by defining state type from implementation. We cannot use interface with this pattern as seen in the code below:

Also, we cannot extend interface with types created by unions and intersection, so in these cases, we would have to use type alias.

6. Don’t use method declaration within interface/type alias

This ensures pattern consistency in our code as all members of type/inference are declared in the same way.
Also, --strictFunctionTypes works only when comparing functions and does not apply to methods. You can get further explanation from this TS issue.

Consider the code below:

7. Don’t use FunctionComponent

Or its shorthand FC

to define a function component!

When using TypeScript with React, functional components can be written in two ways:

  1. As normal functions as seen in the code below:
  1. Using the React.FC or React.FunctionComponent as seen below: https://gist.github.com/lawrenceagles/310dd40107547a3d3ed08ae782f767cf

Using FC provides some advantages such as type-checking and autocomplete for static properties like displayName, propTypes, and defaultProps. But it has a known issue of breaking defaultProps and other props: propTypes, contextTypes, displayName.

FC also provides an implicit type for children prop which also have known issues.
Also, as discussed earlier a component API should be explicit so an implicit type for children prop is not the best.

8. Don’t use constructor for class components

With the new class fields proposal, there is no need to use constructors in JavaScript classes anymore. Using constructors involves calling super() and passing props and this introduces unnecessary boilerplate plate and complexity.

We can write cleaner and more maintainable React class components by using class fields as seen below:

In the code above, we see that using class fields involves less boilerplate and we don’t have to deal with the this variable.

9. Don’t use public accessor within classes

Consider the code below:

Since all members in a class are public by default and at runtime, there is no need to add extra boilerplate by explicitly using the public keyword.
Instead, use the pattern below:

10. Don’t use private accessor within Component class

Consider the code below:

In the code above, the private accessor only makes the fetchProfileByID method private on compile-time since it it is simply a TypeScript emulation. However, at runtime, the fetchProfileByID method is still public.

There are different ways to make the properties/methods private in a JavaScript class private one is to use the underscore (_) naming convention as seen below:

While this does not really make the fetchProfileByID method private it does a good job of communicating our intention to fellow developers that the specified method should be treated as a private method. Other techniques involve using weakmaps, symbols, and scoped variables.

But with the new ECMAScript class fields proposal we can do this easily and gracefully by using private fields as seen below:

And TypeScript supports the new JavaScript syntax for private fields from version 3.8 and above.

Bonus: Don’t use enum

Although enum is a reserved word in JavaScript, using enum is not a standard idiomatic JavaScript pattern.

But if you are coming from a language like C# or JAVA it might be very tempting to use enums. However, there are better patterns such as using compile type literals as seen below:

Conclusion

Using TypeScript no doubt adds a lot of extra boilerplate to your code but the benefit is more than worth it.

To make your code cleaner and better don't forget to implement a robust TODO/issue process. It'll help your Engineering team get visibility on technical debt, collaborate on codebase issues and plan sprints better.


This post was written for the Managing technical debt blog by Lawrence Eagles - a full-stack Javascript developer, a Linux lover, a passionate tutor, and a technical writer. Lawrence brings a strong blend of creativity & simplicity. When not coding or writing, he love watching Basketball✌️

Top comments (17)

Collapse
 
peerreynders profile image
peerreynders • Edited

Bonus: Don’t use enum

Your "Do this" doesn't even compile because Successful, Failed, and Pending aren't even values yet.

Perhaps you were thinking

type Status = 'Successful' | 'Failed' | 'Pending';

function fetchData (status: Status): void {
    // some code.
}
Enter fullscreen mode Exit fullscreen mode

However the recommended pattern is more like this:

const STATUS = {
  Failed: -1,
  Pending: 0,
  Successful: 1,
} as const;

type Status = typeof STATUS[keyof typeof STATUS];

function fetchData (status: Status): void {
    // some code.
}

const someStatus = STATUS.Successful;
Enter fullscreen mode Exit fullscreen mode

Objects vs Enums

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
thewix profile image
TheWix

Not sure what you are getting at here that can't be said about any other language? What about a world class Typescript developer over the many poor JS or JQuery developers?

Typescript is not a silver bullet but it is certainly better than a massive untyped JS project. Doesn't matter how amazing your JQuery is, because you and your team are very likely not better than a compiler.

Thread Thread
 
Sloan, the sloth mascot
Comment deleted
 
thewix profile image
TheWix

I dunno what 'Typescript' patterns you are referring to. Abusing or overusing interfaces is something I have seen in every language that has Java/C#-like interfaces. It is hard to know what bad code you have run into, but it sounds like your problem is with developers as opposed to Typescript itself.

Collapse
 
pengeszikra profile image
Peter Vivo

Imho, my react works I don't use any class component, just functional component, seems much easier.

Looks like this:

export const TypeOfProductButtonList:FC<{linkedList:IJoinedProduct[]}> = ({linkedList}) => (
  <>
    {linkedList.map(({product}) => <ProductButton product={product} />)}
  </>
)
Enter fullscreen mode Exit fullscreen mode

So I don't know why worst is the FC? Imho that is perfect if interface declared.

I started add typescript added our project, so actually whole JS code contain a few TS type definition - great help. Any way now, our project is state of JS - TS hybrid project, and works fine, even pipeline operator is works under TS.

Collapse
 
jaecktec profile image
Constantin

I agree but in your example you should use VFC since you don't have children

Collapse
 
pengeszikra profile image
Peter Vivo • Edited

I count in our working project (24k line JS/TS) contain only 4 times children use in component.

Collapse
 
develliot profile image
Develliot

All good stuff. I don't see anything wrong with using the FunctionComponent, in fact I would strongly advocate for it especially if you are building a FunctionComponent. In fact I would say use as many built in types as possible before creating your own.

I maintain a Typescript React component library and the biggest mistake I made was not originally using the built in types for all the html components and all their attributes that React gives you for free and just extending them. I was originally creating all custom types from scratch and getting issues reported about properties not working time and time again because I had forgotten something important.

React and it's types are maintained by hundreds of developers and it's silly not to trust many eyes on it vs our own.

Collapse
 
bennycode profile image
Benny Code

Why do you need PropTypes when coding with TypeScript? I would rather use React.FC and benefit from type-checking and autocomplete than not using it for the sake of PropTypes.

Collapse
 
mzbac profile image
Anchen

Not sure all the patterns mentioned as valid for clean code. Very opinionated.

Collapse
 
milkywayrules profile image
Dio Ilham D

very opinionated.

Why would anyone still using class components nowadays, u should refactor to functional component as they stated better to not use class component anymore and use functional instead.

Collapse
 
yevhenoksenchuk profile image
Yevhen
  1. We dont use class components last 2 years. 2 enum isnt for declare types. It useful in other cases
  2. Dont use FC, some of cases doesnt work without FC, and generic types to declare props. Ts knows about children as default, we dont need declare it
Collapse
 
glowkeeper profile image
Steve Huckle • Edited

If you want 'private', use closures. Something like:

const friends = () => {
  fetchProfileByID () {} // this probably gets called at initialisation by useEffect

  function render() {
    return // jsx blob
  }

  return { render }
}

const myFriends = friends
myFriends.render()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jbaez profile image
jbaez

Good article, I agree with most points.
I'm also a strong believer that TypeScript helps improving the code quality and maintainability. However, regarding 4. Use type inference for defining a component state or DefaultProps, I would say it might make it cleaner and readable, to define a single interface (or type) for the props, and after create the defaultProps object based on that interface. The way I do it is use a generic type, that defines a readonly type with all the properties as non-optional. These are the generic types that I use for that: gist.github.com/jbaez/71df88c47362...

I recently wrote an article using those generic types, it's about separating the logic of the component from the the UI (React), which also helps making the component/screen cleaner, specially on complex ones : dev.to/jbaez/decoupling-the-logic-...

Collapse
 
rbrtbrnschn profile image
Robert Bornschein

Great concept overall. But #5 is, simply put, is quite misleading.

A interface can be a type but a type cannot be an interface. A square is always a rectangle but a rectangle isn't always a square.

This article explains it well enough.
blog.logrocket.com/types-vs-interf...

Collapse
 
vijayiyer profile image
Vijay Iyer

I can't see the 'code below' in any of the points. Am I missing something?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.