Fence your TypeScript, for saner project boundaries
Tags: typescript, tutorial, webdev, codequality
Published: 2020-06-09 16:40:33

Originally posted at [zohaib.me](https://zohaib.me/fence-your-typescript/)

> "Good fences make good neighbors."                                              — Robert Frost, Mending Wall

Working on large projects has its problems of scale. Certain languages provide solution to these problems and other rely on tools in ecosystem to solve them. This post is a solution to two such problem in TypeScript.

When you have 10s - 100s of developers working on a single TypeScript codebase, who are part of larger organization, contributing across various projects in the code, dependency creep becomes a problem as the boundaries of various projects and modules isn't clear. Even with code reviews it becomes harder to track dependencies and contracts of your module (distinguish between what is private and what is public).

We are going to **solve two problems** here

1. Have explicit boundary of package i.e. what is importable from other packages and what is internal to package.
2. Dependency creep i.e. explicitly call out dependencies of a package so its easier to manage them

In a language like C# you have DLLs and the modifier `internal` which makes it clear what are the various projects you are using and what can be used from those projects without taking dependency on anything private to that. We can have a similar boundary in TypeScript using [path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) and a tool [good-fences](https://github.com/smikula/good-fences).

Let's look a the structure of an example project in which we create a full name using `fullName` function and log it in console.

```
packages 
|__ app 
|    |__ lib
|         |__ index.ts
|__ hello
     |__ lib
          |__ index.ts
          |__ fullName.ts
```

Here `app` and `hello` will act like packages in the project and `app` will take a dependency on `hello` to generate a full name.

```
import { fullName } from 'hello';

function app() {
    console.log(fullName('Zohaib', 'Rauf')); 
}

app();
```

`hello` is not a NPM package but instead we use the TypeScript path mapping capability which allow us to refer to a certain directory/path using an alias.

The aliases can be turned into relative paths using Webpack.

## Boundary of package
Now we have the package defined and use it. We expect that whoever will use the package will always refer to it using `hello` so it refers to _hello/lib/index.ts_ and never refer to any of internal files e.g. _hello/lib/fullName.ts_. We rely on everyone doing the right thing which in practice is hard when you have 10s - 100s developer working. 

Nothing is stopping them using it in these ways hence taking a hard dependency on the location of an internal function

- hello/lib/fullName
- ../../hello/lib/fullName

Now lets use good-fences to solve this problem. We need to create a file `fence.json` in a folder which we want to fencify.

```
{
    "tags": ["hello"],
    "exports": ["./lib/index"],
    "imports": [],
    "dependencies": []
}
```

Here `tags` refer to the tag associated with this project so that other `fence.json` can refer to this package. We explicitly call out what is exported from this package and what are its dependencies. Here `imports` refers to other packages within the project (their fence.json tag) which it uses and `dependencies` are the NPM packages this package uses.

We'll use `gulp` to run good-fences before I compile so the build breaks at compile time

```
const goodFences = require('good-fences');

gulp.task('check-fences', function() { 
	return new Promise((resolve, reject) => { 
    	const result = goodFences.run({rootDir: './'});
        if (result.errors && result.errors.length > 0) {
            const message = result.errors
                .map(err => err.detailedMessage)
                .join('\n'); 
            reject(new Error(message)); 
        } else { 
            resolve(); 
        }
    }); 
});
```

We use `run()` method which goes over all the fence files and ensure the boundaries are respected.

Lets see what happens when we use `import { fullName } from '../../hello/lib/fullName'`

![Import failing](https://dev-to-uploads.s3.amazonaws.com/i/gl4uiwtnc3ylxduhq8n8.gif)

## Dependency creep
We have figured out what gets exported which solves the problem of what is internal to package and what is external i.e. importable by others. Now lets look into how we can solve the dependency creep by having fences for imports.

```
{
    "tags": ["app"],
    "exports": ["lib/index.ts"],
    "imports": [],
    "dependencies": []
}
```

Here we explicitly call out that there is no dependency or import, in app package and lets see what good-fences throws. Recall we use `hello` in the package.


![Import failing](https://dev-to-uploads.s3.amazonaws.com/i/7ns7004oxr3pdaw69zaf.gif)

This can be solved by adding `"imports": ["hello"]` and now we have made an explicit decision to use that as dependency. During code review you can see the file changing and make sure this dependency is needed or not.

If I start using a NPM package, lets take example of using `is-odd` (because I'm lazy to write one line function :P) then your fence file will look like below

```
{
    "tags": ["app"],
    "exports": ["lib/index.ts"],
    "imports": ["hello"],
    "dependencies": ["is-odd"]
}
```

## Incremental adoption
You don't have to adopt good-fences across all your codebase to start seeing its advantages. You can start small and then add fences as you go along. You can add `fence.json` on any new package you create and in this way you can start fixing things as you go along.

I would recommend going over the [documentation](https://github.com/smikula/good-fences/blob/master/README.md) of good-fences to learn more about it.

## Resources
- Example project: [https://github.com/zabirauf/fence-your-typescript](https://github.com/zabirauf/fence-your-typescript)
- [Github - good-fences](https://github.com/smikula/good-fences)
- [TypeScript path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping)