Jay Gould

The fundamentals of building a web app with React and Typescript

March 11, 2019

Introduction

A few months back I wrote a quick reference guide for Typescript. As it was so popular, I decided to jump further in to Typescript and use it alongside React to build a project I’ve been wanting to get out my system for a while - a list app. There are plenty of list apps out there, and to be honest mine isn’t much different. I wanted it to do something specific, but I’ll get more in to that later.

As I got deep in to my project I decided to do a lot of refactoring. I’d learned a lot along the way, and looked back at the first few React Typescript components I’d built, realising there was a much better path to go down in various aspects.

This post will cover the learnings I have made along the way, some best practices that I have come to realise, and will come from the angle of a beginner because I feel a lot of the posts out the already were a bit too much for me at the time. Hopefully this will help fill the gaps for beginners.

About Listy app

As I mentioned before the web app I have built is a list app, and the reason I wanted to develop such an application is to log my favourite beers alongside their rating out of 10, and an photo of each one. As time went on, I wanted to use the ability to tag list items with images for other types of lists, such as todo lists.

So the app started to take shape as a list of lists essentially. You can create a list called, “Favourite beers” for example, and this will allow you to create list items within that list. Then you could make another one for “Shopping” for example, and have a shopping list within that.

Each list can have an image uploaded to it, and also an image uploaded to each of the list items within each list.

Listy

Another requirement for the app was to be mobile-first. I wanted to mainly use it on the go, and leverage the power of local storage and service workers to make the app a Progressive Web App (but more on that part of the app in another post coming soon).

The app has a custom built authentication system to handle multiple users, as well as the ability to add different features easily in future, such as the ability to share lists with other users, tag location etc.

Although there are many (many) list apps out there, I was really using it as an exercise to get a deeper understanding of Typescript and a few other technologies alongside. A list of all dev technologies involved are:

  • Typescript
  • React
  • Apollo Client (React Apollo)
  • Node Express
  • Apollo Server
  • JWT for authentication

I’ll only be going in to detail about the React and Typescript part of the app in this post, but feel free to browse the Github repo to see the integration of the other technologies. The web and server parts of the app are included in the same repo.

React and Typescript

Now the back story is out the way, here’s the run down of what I wanted to share about my experience with React and Typescript.

Use the TypeScript-React-Starter project from Microsoft

Installing Typescript to a React project isn’t that difficult. It requires installing Typescript and React Types, and editing your package file. However, if you’re just starting out, it might be best to use the template starter project from Microsoft. It’s so easy to install, and has the recommended project structure and tsconfig.json & tslint.json settings all configured for you.

Just in case you don’t know, the tslint.json file works much like jslint. The TypeScript-React-Starter package’s recommended linting is quite strict, and while this is helpful to learn the do’s and dont’s, it can be too strict. For example, it does not compile the Typescript if there is a console.log() in your code. It’s best not to leave them during production, but perhaps best to ignore this during development.

Here’s a snippet of my tslint file:

{
  "extends": [
    "tslint:recommended",
    "tslint-react",
    "tslint-config-prettier"
  ],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts"
    ]
  },
  "rules": {
    "object-literal-sort-keys": false,
    "jsx-no-lambda": false,
    "member-access": true,
    "no-console": false
  }
}

Using Types - the basics

All variables, function parameters, and pretty much anything that can be defined, needs to have a corresponding type to check against to ensure “type safety”, so code is likely to run without silly errors like “Cannot read property of undefined”.

//It’s also a massive help when using your IDE as features like IntelliSense, as it allows you to see function argument //types, available methods on classes, available imports etc.

Types are assigned using the colon :[type] annotation. For example, assigning a type of string to a variable:

let userName: string = 'Dave';

Assigning types to objects:

// define object type
let userObject: {
  name: string,
  phone: number,
}

// add object values according to the defined type above, else an error will be thrown
userObject = {
  name: "Stephen",
  phone: 012345678,
}

Assigning types to arrays:

// define an array of strings
let userArray: string[]

// add array values to array
userArray = ["Jennifer", "George"]

The above works for basic types, but more complex applications often have complex data types which need to be typed in multiple places in an application. This is where Interfaces come in. Interfaces allow us to set our own types for things like “listName” and “listImageUrl”, like so:

// define an interface which is an object in this case
interface IList {
  list: {
    listName: string,
    listImageUrl: string,
  };
}

// create a new variable with the "IList" type, which must contain the exact type outlined in the definition above
let list: Ilist = {
  list: {
    listName: "Beer list",
    listImageUrl: "https://example.com/img.png",
  },
}

There’s a feature of Typescript called a “type alias” which you may see around. This is very similar to an interface but has some differences. See this SO post for more info.

Using types with React and other libraries

Third party libraries come with their own types which are already made by themselves or other people. An example I find useful to illustrate this is the react-router-dom’s history object.

history has a few functions such as back(), go(), push() etc, and each of these may take arguments (which need to have types defined) and also may return a value (which needs to have a type). All of these types are defined in a type definition file, which can be installed from npm using npm i @types/history.

It’s worth noting that for react-router-dom, the history types can also be used by extending RouteComponentProps which comes as part of React Router DOM. See this link for an explanation. I am installing the history types separately here to illustrate show how to install types generally.

Once installed, you need to include the type. If you begin to type “History” inside the corresponding interface where you want to assign the definition, you’ll see (if using VS code) that an option shows to import the History type and sort it all out for you:

Typescript history type

Once the History type is imported and when you type “history.” in to your component, it will show you the available functions and other types defined on that history object.

History is a simple example, and as types are needed for everything it can be tempting to use the “any” type which tells the compiler that any type will pass type safety. This is not always a good idea as you’ll loose type safety down the chain of elements/functions etc. that are dependent on the “any” type. Trust me when I say it’s worth using the correct type in the long run, as it will save you a headache, as well as give you the full benefit of Typescripts intelligence in recognizing function types, parameters etc.

Using Interfaces and keeping them organsied

I’ve briefly covered the Interfaces earlier, but here I want to share how I have started structuring and organising them.

Ultimately the point of an Interface is to encapsulate the type declarations of a complex data type in order for Typescript to understand what information is being used around your application. This could be a User interface which contains a name, age and address for example. Or, more relevant to my example here, is a List interface.

As I developed my list app to be more complex, I found myself typing in the same interface data for my List interface over and over. I also found that I was splitting my interfaces out into smaller, logically contained ones which related to something specific. For example, instead of having a List interface which contained the following:

export interface IList {
  id: string;
  listName: string;
  listType: string;
  listImage?: string;
  maxRating?: string;
  dateAdded?: string;
  thisList: {
    id: string,
    item: string,
    completed?: boolean,
    notes?: string,
    listItemImage?: string,
    dateAdded?: number,
  }[];
}

I split them out to separate interfaces:

export interface IList {
  id: string;
  listName: string;
  listType: string;
  listImage?: string;
  maxRating?: string;
  dateAdded?: string;
  thisList: IListItems[];
}
export interface IListItems {
  id: string;
  item: string;
  completed?: boolean;
  notes?: string;
  listItemImage?: string;
  dateAdded?: number;
}

Don’t forget, interfaces can be used inside of each other which is a very common use case for situations exactly like above.

I have interfaces for other things too such as function arguments. Take the following example:

interface IProps {
  addListItem: ({ id, item, notes }: IAddListItemFormVals) => void
}

This is an interface for the props of a React component. The components props contains a function, addListItem which is passed down via a GraphQL Higher Order Component. The fact that it’s GraphQL is irrelevant really - the important part here is that the addListItem function takes one argument which is an object containing the list item information, and that object has a defined type, IAddListItemFormVals.

I could have typed out the type declaration for each item in the object, such as { id: number, item: string, notes: string }, but now that an interface exists, I can re-use it whenever I am typing out arguments to add a list item. This comes in handy for adding types to a form or GraphQL HOC functions.

One final thing to mention on interfaces is where to keep them. It’s best practice to keep them in files that have a .d.ts suffix. My IList and IListItems interfaces are in src/interfaces/list.d.ts. I have a separate file to handle GraphQL specific interfaces, and all interfaces export from their file and are then imported to an index file at /src/interfaces/index.d.ts. That way, all my interfaces can be imported from one location:

import { IList, IMarkListItemArgs } from '../interfaces';

There’s a lot of different use cases and ways to use interfaces, even just in React, so head over to my Github repo to see the bigger picture.

Split out component baggage where you can

This may seem like an obvious one because it’s common practice for React in general, but it’s perhaps more important for Typescript that code is kept clean as you’ll likely have Typescript “type” and/or “interface” declarations which can cause your files to get bloated. My list app also uses Apollo (the older HOC Apollo because I prefer it to the new 2.1 version), so there’s additional blocks of query and mutation code linked to components also.

My general rule of thumb is to import everything other than the actual component from another directory. This can mean styles, Redux reducers/actions, Typescript interfaces etc. Here’s an example:

import * as React from 'react';
...// more imports for Interfaces
...// more imports for GraphQL queries and mutations

interface IProps {
  list: { listName: string, listType: string }
}

interface IState {
  modal: {
    ... // modal state
  }
}

class ListItems extends React.Component<IProps, IState> {
  constructor(props: IProps) {
    super(props)
    this.state = {
      modal: {}
    };
	}

	public render() {
    const { list } = this.props;
    const { modal } = this.state
    return list ? (
      <div>
        ... // list stuff
      </div>
    ) : (
      null
    )
  }
}

You’ll notice I’ve kept the IProps and IState Interfaces above the component. I prefer to do this with React as the props and state are so closely linked to a component, it’s nice to have them directly above for quick reference.

Keep heavy logic in a service class or file

Again, not really specific to Typescript and is a rule of thumb for programming in general, but it helped me learn Typescript easier when I was creating classes for things like authentication and image uploading. I have the following classes for example, which can be found in the repo:

  • ListService - handles uploading the list images to Cloudinary.
  • AuthService - handles log in, log out, and saving JWT’s to local storage.

Things like authentication and image uploading are quite big pieces of functionality. At the time of writing, these are going to only increase in size and complexity, so I prefer to keep them out of components completely. This promotes re-use of code and helps keep the React part of the application more lean.

When creating classes in Typescript, you’ll again be required to define types for methods and method arguments, as well as and variables within the class. It’s the same as working with React in Typescript for the most part in terms of syntax and general concept, but working in a logic-based environment of a normal Typescript class (as opposed to the “UI-based” environent of React) may lead you to explore more parts of Typescript such as generics, enums, and more advanced use cases. I’ll cover some of this later on in this post.

Using stateless components

As with non-Typescript based React, components can be stateless or stateful. One interesting thing about Typescript with React is how these components are defined. You can go ahead and make a normal function, containing no state:

interface IProps {
  addList: ({ listName, listImage }: IAddListFormVals) => void
  children: ReactNode
}

const listItem = ({ addList }: IProps) => (
  ...// component contents
)

But this is just a normal function now, not connected with React at all. However as I briefly touched on before, most libraries have types available for us to use, and React has a type specifically for stateless functional components (SFC’s):

interface IProps {
    addList: ({ listName, listImage }: IAddListFormVals) => void
}

const listItem: React.SFC<IProps> = ({ addList, children }) => (
  ...// component contents
  ...// do stuff with new "children" prop now avalaible to us
)

You’ll notice that now we are using the SFC component, the children prop no longer needs to have a type definition in our interface - React sorts that out for us. If you’re on VS Code, hold down the cmd key and click on the SFC type on the component as this will take you through to the types listed for the SFC:

Typescript history type

You can see that we’re able to retrieve types for all sorts!

Using stateful components

Stateful components aren’t much different, having the additional ability to set and get state like normal React. The state and props of a component are defined and passed in to the component like so:

interface IProps {
  list: { listName: string, listType: string }
}

interface IState {
  modal: {
    ... // modal state
  }
}

class ListItems extends React.Component<IProps, IState> {
  constructor(props: IProps) {
    super(props)
    this.state = {
      modal: {}
    };
  }

  public render() {
    const { list } = this.props;
    const { modal } = this.state
    return (
      ...// component contents
    )
  }
}

The props and state in the above example are passed down to the component in the angle brackets, similar to the React.SFC example earlier. Just to show an alternative, this can be written inline instead of using interfaces. For example:

class ListItems extends React.Component<{list: { listName: string, listType: string }}, IState> {
  constructor(props: IProps) {
    super(props)
    this.state = {
      modal: {}
    };
  }

  public render() {
    const { list } = this.props;
    const { modal } = this.state
    return (
      ...// component contents
    )
  }
}

Recognizing and understanding generics

I found generics the most confusing part about typescript initially, mostly because of their angle bracket notation. The angle brackets shown in the above React component examples are generics, which are an important part of typescript. Generics are not to be confused with type assertion though, as they look similar.

Type assertion

A type assertion is when you can “assert” a type to a value. It allows you to say to the compiler that it should treat a value with a certain type instead of using it’s current type, or the type that Typescript will naturally infer. Here’s a type assertion:

let code: any = 123;
let employeeCode = <number> code;
console.log(typeof(employeeCode)); //Output: number

The above example is taken from a source which I think explains type assertions really well, so I won’t go in to much detail. An important thing to note is that as React uses JSX, the above angle bracket notation is not allowed, so instead you must use the “as” version of type assertion. This is all explained in the link above.

Back to generics

Generics allow you to specify the argument of a function or class to be “generic”. When using the function, you’re then able to use said class or function with different types rather than being restricted to just one type.

function logValue<T>(arg: T): void {
  console.log(arg)
}

The above example is the most simple hello world of generics, but it’s worth showing. The T specifies the generic type, so in this example the logValue function takes a generic type in the angle brackets, which is then used as the type for the argument of the function. The logValue function example usage would be:

logValue < string > "Log this"

As with many hello world examples, they are mostly useless because they don’t show the real life implementations. The official docs are great at explaining generics, but I wanted to show a few different ways they are used.

Used in a stateless component:

interface IProps {
  addList: ({ listName: string, listImage: string }) => void
}

export const AddList: SFC<IProps> = ({ addList }) => ();

// Usage inside component:

addList({ listName, listImage });

The interface passed to the SFC defines the type of the function argument.

Used in a stateful component:

interface IProps {
  addList: ({ listName: string, listImage: string }) => void;
}

class ListItems extends React.Component<IProps, {}> {}

The interface here is the generic type for the React component class.

Used for an interface:

I think real world examples are better to explain generics. The below interface is defined in Formik - a popular form handling tool for React. You can see the interface takes takes a generic argument, V, which has a default type of any:

Typescript interface within interface

The FieldProps interface is used like this:

export interface IFormVals {
  id: string,
  item: string,
  dateAdded?: number
}

... // a single form field
render={({ field, form }: FieldProps<IFormVals>) => (
  <div></div>
)}
...

The interface here, IFormVals, is being passed as a generic type to the FieldProps interface. This shows how an interface can be used within an interface to create a type which is part custom and part pre-defined. In this example with Formik, the FieldProps interface allows a single type to comprise of types for the single form field, and also for the entire form, including all current form fields.

Apollo actually takes generics and interfaces one step further. As their components and functions are typed in Typescript, you are required to implement Typescript heavily, as shown in these examples. Using Apollo with Typescript is a great way to get your hands dirty with the more complex parts (in my opinion).

That’s it!

Thanks for reading - drop me an email or Tweet if you notice anything which doesn’t make sense.


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.