One Small TypeScript Import/Export Change That Saved Us
Tags: javascript, typescript
Published: 2020-06-16 13:38:36

We added TypeScript (TS) to our React + Ruby on Rails app about 4 months ago now. We have reached a decent tipping point where I'm not the only one who understands how to use TypeScript and other devs are actively converting non-TS files over to TS without much guidance. 

One hurdle we've recently faced, is that as we add more types and shared types across multiple directories in our frontend, knowing WHERE a type lives and IF a type already exists has become really challenging. We've run into almost virtual duplicates of types multiple times now. 

I said, "I've had enough! This is supposed to be helping us be less confused - not add to our confusion."

> How can more structure be added to our storage of types so that it's easier for all of us to know where to check if a type exists already or not?

With some fiddling around, a little reading, and a little battling of our linter and we came to a pretty great solution I'm happy to document here. 

## The Problem
For some context, we have a frontend that's broken up into three "apps" and then shared helper files kind of like this: 
```
bakery/
├── cookies/
│   ├── Cookie.tsx
│   ├── Biscuit.tsx
│   ├── lib/api.tsx
├── cake/
│   ├── Cake.tsx
│   ├── BirthdayCake.tsx
│   ├── lib/api.tsx
├── pudding/
│   ├── Pudding.tsx
│   ├── lib/api.tsx
└── shared/
    └── types/
```

So what has organically happened (and what is a little hard to show with his very simple example) is that components inside `/cookies` for example have some of their types locally defined since only those components need access to those types. So you might have an import statement like:
```javascript
// bakery/cookies/SomeComponent.tsx

import { ChocolateChipType } from 'bakery/cookies/Cookie'
```

But what if you wanted a Chocolate Chip Cake? Then you'd have to access the Chocolate Chip type from the cookie directory and that's where things start to get really messy.

> For us, as soon as we started to leverage shared types and generics, our organization and structure went out the window. 

So, a new day began and I was determined to figure out how to make our types more easily shareable while also reducing the places you had to check for where the type might be defined. 

## The Solution
### Option 1: Namespacing (deprecated)
I'll be honest - this was the first solution I came up with. I liked it - but our linter hated and informed me was a deprecated method 😅 .
We use `"react-scripts": "3.4.1",`. 

🔖 [Namespacing and Module TypeScript Documentation](https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html)

To me, namespacing is a way of grouping similar things together under one umbrella name. So you could have a namespace of `Cookies` and then nested types of `ChocolateChip`, `Sugar`, `Overcooked`, etc. 

A file using this method might look like this: 
```javascript
// bakery/cookie/types.ts

export namespace Cookies {
    export interface ChocolateChip { /* ... */ }
    export class Sugar { /* ... */ }
}
```

Then, if I want to use the ChocolateChip type, I could import and use it like this:
```javascript
// bakery/cookies/Oven.tsx

// import full namespace
import { Cookies } from 'bakery/cookie/types'

// access specific type via dot notation
function CookieOven({ cookie }: Cookies.ChocolateChip) {
  return (...)
}
```

This method has the benefit of great autocomplete for your IDE - as soon as you type `Cookies.` you should see a list of all the types defined under that namespace. 

Again - I personally liked how this looked, how it worked, and it made sense to me. BUT my linter did not like it and I had to start over with a different idea. 

Depending on the version of eslint you're using, you might be able to use this method, but honestly I wouldn't recommend it since you'd have to upgrade and adapt eventually. 

### Option 2: ES6 Modules
This should look familiar to most of you since it's probably the way you import React to each of your components: 
```javascript
import * as React from 'react'
```

While it's nothing revolutionary, the way we leveraged ES6 modules are what made this solution make sense for us. 

So, back to our type definition file, we remove the `namespace` and individually export all of our types and interfaces: 
```javascript
// bakery/cookie/types.ts

export interface ChocolateChip { /* ... */ }
export class Sugar { /* ... */ }
```

Because of the structure of our app, we actually have multiple segmented type files in one directory. That looks something like this: 
```
bakery/
├── cookies/
│   ├── Cookie.tsx // No more types defined here
│   ├── Biscuit.tsx
│   ├── lib/api.tsx
├── cake/
│   ├── Cake.tsx
│   ├── BirthdayCake.tsx
│   ├── lib/api.tsx
├── pudding/
│   ├── Pudding.tsx
│   ├── lib/api.tsx
└── shared/
    ├── types/ // all types defined here
        ├── cookie.ts
        ├── cake.ts
        └── pudding.ts
```
I don't know about you - but when you have an app that uses a lot of modular components that can be put together in a variety of ways for different use cases, you start to get import fatigue. 

Even once I moved the types into the `shared/types` directory, I was still having to import types across multiple files like this: 
```javascript
// bakery/cookie/SomeComponent.tsx

import { ChocolateChip } from 'bakery/shared/types/cookie'
import { Tiramisu, DeathByChocolate } from 'bakery/shared/types/cake'
```

Not terrible - and this works! - but pretty annoying if you want to use types from multiple modules in one file. 

To try to get down to one import statement, you can add a default export file: `index.tsx` and then the magic starts to happen. So let's add that file:
```
bakery/
├── cookies/
├── cake/
├── pudding/
└── shared/
    ├── types/
        ├── cookie.ts
        ├── cake.ts
        ├── index.ts // This file is important!
        └── pudding.ts
```

And update it to be our main source of exporting all types: 
```javascript
// bakery/shared/types/index.ts

import * as Cookie from './cookie'
import * as Cake from './cake'
import * as Pudding from './pudding'

export { Cookie, Cake, Pudding }
```

So this is saying import everything from those files under the alias `as XYZ` and then export them as an object literal. The functional behavior of that export statement is similar to:
```javascript
export {
  Cookie: Cookie, // key is "Cookie" and value is the exported module of Cookie
  Cake: Cake,
  Pudding: Pudding
}
```

So now! Let's go check out how that changes our import statement in our component: 
```javascript
// bakery/cookie/SomeComponent.tsx

// OLD WAY
// import { ChocolateChip } from 'bakery/shared/types/cookie'
// import { Tiramisu, DeathByChocolate } from 'bakery/shared/types/cake'

// NEW WAY
import { Cookie, Cake } from 'bakery/shared/types`
```

🎉 That's it! We can still access individual interfaces from those imports like `Cookie.ChocolateChip`. But now, we also get autocomplete for the import statement - so you know which modules you have access to while you're importing. 

It's not very exciting or even a new idea in the world of JavaScript - but let me tell you - this small change has saved us so much time, reduced our cases of duplication, and made it really obvious WHERE our type definitions should live and how we manage them.