---
title: You might not need TypeScript project references
date: 2021/04/23
description: As it turns out, you might not even references or even an interim TypeScript build step with a pattern I am about to show you, which I dub "internal packages."
tag: web development
author: Jared Palmer
ogImage: /blog/og-project-references.png
type: post
---

If you've worked in a larger TypeScript codebase or monorepo, you are likely familiar with [project references](https://www.typescriptlang.org/docs/handbook/project-references.html "TypeScript Project References Documentation"). They are indeed fairly powerful.

When you reference a project in your `tsconfig.json`, new things happen:

- Importing modules from a referenced project will instead load its output declaration file (`.d.ts`)
- If the referenced project produces an `outFile`, the output file `.d.ts` file’s declarations will be visible in this project
- Running build mode (`tsc -b`) will automatically build the referenced project if it hasn't been built but is needed
- By separating into multiple projects, you can greatly improve the speed of typechecking and compiling, reduce memory usage when using an editor, and improve enforcement of the logical groupings of your program.

Sounds awesome! Right?! Well...maybe. Once you add references to your project you now need to continuously update them whenever you add or remove packages. That kinda blows.

Well...what if you didn't need to?

## "Internal" TypeScript Packages

As it turns out, you might not even need references or even an interim TypeScript build step with a pattern I am about to show you, which I dub "internal packages."

**An "internal package" is a TypeScript package without a `tsconfig.json` with _both_ its `types` and `main` fields in its `package.json` pointing to the package's untranspiled entry point (e.g. `./src/index.tsx`).**

```json
{
  "name": "@sample/my-internal-package"
  "main": "./src/index.ts"
  "types": "./src/index.ts", // yes, this works!
  "dependencies": {
     ...
  },
  "devDependencies": {
     ...
  }
}
```

As it turns out, the TypeScript Language Server (in VSCode) and Type Checker can treat both a raw `.ts` or `.tsx` file as its own valid type declaration. This last sentence is obvious once you read it twice. What isn't so obvious, though, is that you can just point the `types` field directly to raw source code.

Once you do this, this package can then be used _without_ project references or a TypeScript build step (either via `tsc` or `esbuild` etc) as long as you adhere to 2 rules:

- **The consuming application of an internal package must transpile and typecheck it.**
- **You should never publish an internal package to NPM.**

As far as I can tell, this internal package pattern works with all Yarn/NPM/PNPM workspace implementations regardless of whether you are using Turborepo or some other tool. I have personally tested this pattern with several different meta frameworks (see below), but I'm sure that it works with others as well.

### Next.js

If you use Next.js, you can satisfy these constraints with the `next-transpile-modules` plugin which will tell Next.js to run certain dependencies through its Webpack/Babel/TypeScript pipelines.

### Vite

Internal packages just work. No extra config is needed.

### React Native

If you use [Expo](https://expo.io) and use the [`expo-yarn-workspaces`](https://github.com/expo/expo/tree/master/packages/expo-yarn-workspaces) or [`@turborepo/adapter-expo`](https://www.npmjs.com/package/@turborepo/adapter-expo) package, you can just use internal packages as long as you are targeting iOS or Android. When you run Expo for these platforms, all of `node_modules` are automatically transpiled with [Metro](https://facebook.github.io/metro/). However, if you are targeting Expo for web, internal packages will not work because `node_modules` are oddly not transpiled for web.

_I reached out to the Expo team about this inconsistency. They are aware of it. It's a legacy wart I'm told._

## The beauty of this pattern

This pattern rocks because it saves you from extra needless or duplicative build steps. It also gives you all the editor benefits of project references, but without any configuration.

## Caveats

When you use an internal package, it's kind of like telling the consuming application that you have another source directory—which has pros and cons. As your consuming application(s) grow, adding more internal packages is identical to adding more source code to that consuming application. Thus, when you add more source code, there is more code to transpile/bundle/typecheck...so this can result in slower builds of the consuming application (as there is just more work to do) but potentially faster (and less complicated) overall build time. When/if overall build time begins to suffer, you might decide to convert your larger internal packages back into "regular" packages with `.d.ts` files and with normal TypeScript build steps.

As previously mentioned, this pattern actually has very little to do with Turborepo. It's just super duper awesome and I think you should be aware of it. As we are actively working on preset package build rules (i.e. "builders") for Turborepo, we'll using the internal package pattern to skip build steps.

## Speaking of long build times...

Shameless plug here. If you are reading this post, and you're struggling with slow build and test times, I'd love to show you how Turborepo can help. I guarantee that Turborepo will cut your monorepo's build time by 50% or more. [You can request a live demo right here.](https://turborepo.com/#demo)
