loading...

How TypeScript helped me to self-document my React project

rocambille profile image Romain Guillemot ・3 min read

Let's start with a simple line of JavaScript:

const foo = [];

Well... that's an array declaration, no big deal. Let's now ask a simple question: what will be this array used for?

Hard to say without any context, plus the variable name isn't helping. Here is the context: I'm working on a side project using React — and React Router. Here is the real code, with a real variable name — in a file named pages.js:

export const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
];

Here you are: the array will list the routes details for the application. Then you may import it into index.js to instantiate the routes:

import { routes } from './pages';

ReactDOM.render(
  <StrictMode>
    <BrowserRouter>
      <Switch>
        {React.Children.toArray(
          routes.map(({ component: Component, ...rest }) => (
            <Route {...rest}>
              <Component />
            </Route>
          ))
        )}
      </Switch>
    </BrowserRouter>
  </StrictMode>,
  document.getElementById('root')
);

note: I used React.Children.toArray on the result of routes.map. This generates a key attribute for each element.

Of course, I didn't invent that pattern. I saw a similar code, and wanted to make it a template repository on GitHub. Thus the array should be empty in pages.js. Indeed the template can't declare any route: at the time of writing I don't know the application I will build.

export const routes = [];

I will remember how it should be filled since that's my own code... at least a while. Remember Eagleson's law?

"Any code of your own that you haven't looked at for six or more months might as well have been written by someone else."

Sure I would be thankful to myself 6 months later — or next week — if I add some comment:

/* route format: {path: '/foo', component: Foo} */
export const routes = [];

It may looks ok at first glance, but looking closer it's not. Really not. First, I can't expect anyone — including me — to understand this comment. It describes something as a route format. It doesn't explain what to do. In fact it's only useful if you already know what to do. Good enough for a reminder, but not what you would expect of a documentation.

And there is an other issue. Do you always read comments in code? I don't. Hours of code reading trained my eyes and my brain to ignore them as much as they can. I'm used to see comments as pollution between 2 lines of code.

Let's see the glass half full. Now we have a checklist to write useful documentation:

  • you should express it in a explicit, non-ambiguous form,
  • you should ensure it will be read.

"Explicit, non-ambiguous expression" doesn't look like a definition of writing, but of coding. You can't be sure any human will read what you wrote. But would you ask a program, you can always be sure it will "read" your code. So why not coding the documentation? A code version of this comment:

/* route format: {path: '/foo', component: Foo} */

That's where TypeScript can help us. In a nutshell, you can use TypeScript to describe the type of value expected for a variable:

const anyValue = '42'; // standard JS: will never complain
const anyString: string = '42'; // ok: '42' is a string
const anyNumber: number = '42'; // no: '42' is not a number

That's for primitives. Would you need to ensure an object has specific, typed properties you can define an interface:

interface MyInterface {
  anyString: string;
  anyNumber: number;
}

const myObject: MyInterface = {
  anyString: '42',
  anyNumber: '42'; // again, wrong type !!!
};

And that's what we need. Instead of a not-sure-to-be-useful comment, we can "type" the array using an interface. This will describe its future content:

interface RouteInterface {
  path: string;
  component: React.Component | React.FC;
}

export const routes: RouteInterface[] = [];

TypeScript can't misunderstand it, and it will never forget to read it. Let's try it with an invalid object:

export const routes: RouteInterface[] = [
  { whenToRender: '/foo', whatToRender: Foo }
];

This will output something like that:

Type '{ whenToRender: string; whatToRender: any; }' is not assignable to type 'RouteInterface'
Object literal may only specify known properties, and 'whenToRender' does not exist in type 'RouteInterface'

Here we go: a first using of TypeScript to write self-documenting code.

Discussion

markdown guide