How to use project references in TypeScript 3.0?
Asked Answered
P

2

86

There is a new feature in TypeScript 3.0 called Project References. It suggests better interaction of *.ts modules between themselves.

Can anyone help me understand exactly, what problems does it solve, how does it do that, and how would I benefit from it? I have a project with a similar structure, so it might (or might not) be very helpful for it.

The project structure is roughly:

project/
    lib/
        index.ts # defines the original code
    test/
        index.spec.ts # requires lib/index.ts
    package.json
    tsconfig.json
Paulo answered 1/8, 2018 at 11:9 Comment(0)
P
160

TL;DR:

The feature allows defining parts of the project as separate TypeScript modules. Among other things, this allows configuring those modules differently, building them separately, etc.


Before

Initially, the project structure, when simplified, is similar to this:

/
    src/
        entity.ts # exports an entity
    test/
        entity.spec.ts # imports an entity
    tsconfig.json

An entity is defined in src/entity.ts module, and then used in test/entity.spec.ts file.

Notice that there is only one tsconfig.json file here, sitting in the root folder. This basically says that this folder contains one big solid TypeScript project. This project includes a couple of files, organized in folders; some of those files are used for testing other ones.

This structure however imposes a problem: the process of compiling the project (namely, tsc) also compiles the test files, thus creating dist/test/entity.spec.{js|d.ts} files in the output. This should not happen, therefore the tsconfig.json file is slightly altered to include only those files/folders that are intended for outside usage:

{
    "compilerOptions": {
        // compiler options
    },
    "include": [
        "./src"
    ]
}

This kind of solves the problem, however now all files in /test are completely unrelated to the files in /src. Specifically, /test has its own compiler options; so, for example setting strictNullCheck to true in tsconfig.json will lead to inconsistent errors, because it is false (by default) for the files in /test.

This is obviously very wrong.


After

After utilizing the feature, the project structure has changed to this:

/
    src/
        entity.ts # exports an entity
        tsconfig.json
    test/
        entity.spec.ts # imports an entity
        tsconfig.json
    tsconfig-base.json

Let's go through the changes:

  1. Renaming /tsconfig.json to /tsconfig-base.json is a pretty major thing by itself: the root folder is not a TypeScript project anymore, since tsc requires the tsconfig.json file to be present.
  2. On the other hand, adding src/tsconfig.json and test/tsconfig.json files turns both src and test into two separate TypeScript projects, independent from each other.

The contents of /{src|test}/tsconfig.json files are similar since no changes in the configuration were expected, i.e., the "strictness", the output folder, as well as other such parameters, should be preserved. In order to make them similar without copy-pasting anything, all the configurations are put in an arbitrary file, accessible from both places; in this case, the tsconfig-base.json in the root folder was selected for that:

// the contents of /tsconfig-base.json
{
    "compilerOptions": {
        // compiler options, common to both projects
    }
}

This file is being "inherited" then by /{src|test}/tsconfig.json files, with addition of any other options if needed:

// the contents of /{src|test}/tsconfig.json
{
    "extends": "../tsconfig-base.json",
    "compilerOptions": {
        // additional compiler options, specific to a project
    }
}

Notice how this pattern is similar to defining an abstract class with incomplete implementation, and then extending it by two separate "concrete" classes.

Now, /src and /test folders basically hold two separate TypeScript projects with similar configurations. The last thing to do is to specify the relation between the two. Since test depends on src, the test has to somehow "know" about src. This is done in two pretty obvious steps:

  • allow src to be "referenced" from the outside by declaring it as "composite":

    // in /src/tsconfig.json
    {
        "extends": "../tsconfig-base.json",
        "compilerOptions": {
            // compiler options
            "composite": true
        }
    }
    
  • reference src from test:

    // in /test/tsconfig.json
    {
        "extends": "../tsconfig-base.json",
        "references": [
            { "path": "../src" }
        ]
    }
    

The "include" array in /tsconfig-base.json is not needed now, since the code exclusion is done by "drawing new borders".

UPDATE: the following section seems to be outdated since TypeScript 3.7

Now, the test project requires *.d.ts files for the src project to be present. This means that before running tests, the src should already be built, separately. This is done by using the new mode of tsc, triggered by the --build option:

tsc --build src

This command builds the src project and puts the output in the specified output folder (in this case, /dist), without neither breaking test nor losing any compile errors.

Paulo answered 5/3, 2019 at 10:20 Comment(11)
Thanks for taking the time to write this Dmitry, I appreciate your insight.Dumbstruck
I wish the official docs were so clear as this answer. Thanks!Macroclimate
Can you show actual code in the test directory? Is path significant here as in we import { myFunction } from "path". Feel like this answer is missing a crucial piece.Collen
@Collen There is a couple of links to the project, where it is used. I've edited the answer to add a couple more, and make them more obvious. Also, the source of the project is open, feel free to further investigatePaulo
Still no example of importing. A link to gitlab is not sufficient.Remainder
@ChrisFremgen I'm not completely sure, what exactly is missing. Is it the usage of export and import statements? If so, it is not changed; opting-in to Project References doesn't change that syntax. I thought that's obvious and just made a link, instead of copying the code. If you still feel like the code should be directly present in the answer, please let me knowPaulo
I had to add "paths": { "@my/theme": ["../theme/source/"] } as well as "references": [{ "path": "../theme/" }] to get it to work. Builds that don’t change anything run in less than a second now. Hope this helps someone.Exaggerated
So...project references have nothing to do with module resolution, is that right? This is only to cut down on compilation time by telling the compiler "I depend on project A, and if that is up to date, we can go faster" ?Gemina
What if the reference project has its own dependencies specified in a package.json file? I am running into bit of a problem and I have posted a question #67070755Hatpin
@ParzhfromUkraine What's actually missing is the actual usage. :-) As others have mentioned, "paths" can be used to comfortably refer to the sibling project. The problems is not with building but with running. Package.json's equivalent to "paths", "import" doesn't allow reference to outside the package, that is, you can't reference the sibling project when running. It compiles but it doesn't run.Rysler
@Gábor Runtime issues are not only out of scope of this question or this feature but also out of scope of TypeScript itselfPaulo
G
3

It's for TypeScript libraries you develop, that are used by other TypeScript application. So for example, if you make some util library like lodash but are actively developing it along side your dependent application, the references in ``tsconfig.json``` lets you reference the source code, and have your dependent application be rebuilt automatically when the util source changes (i.e.: tsc detects source code changes in the util ts lib)

In my case specifically, I use the references in conjunction with npm link and git submodules and it's working out a lot better than in the ts 2.x days.

Glamour answered 8/9, 2018 at 17:4 Comment(2)
I've added a rough representation of the project structure. If I understood your answer correctly, it would make sense to create project/test/tsconfig.json file and specify project/lib/index.ts in its references, right? This looks a bit weird, so correct me if I'm wrong.Paulo
@DmitryParzhitsky you can look at my open source typescript project for an example. search npm for "xlib" and "phantomjscloud". the first is the library, the 2nd uses it. Locally I have phantomjscloud reference xlib via npm link xlibGlamour

© 2022 - 2024 — McMap. All rights reserved.