Search code examples
typescriptecmascript-6tsconfig

What happens if tsconfig 'targets' and 'libs' are mixed among dependencies for TypeScript projects?


I am building an Electron app that imports a module called foo that imports a module called bar. I maintain all three projects.

Historically (but without a reason) all three projects have different values for target and lib in their tsconfig.json.

Electron App tsconfig.json:

"target": "es5",
"lib": ["es2017", "es2016", "es2015", "dom"]

node_modules/foo tsconfig.json:

"target": "es2021",
"lib": ["es2019", "es2016", "es2015"]

node_modules/bar tsconfig.json:

"target": "es2017",
"lib": ["es2015"]

Question: Given a TypeScript project, what effect do its target and lib values have on the transpilation of its files and on the transpilation of files of other projects which depends upon it? What kinds of problems could occur with mixed values between a dependent and its dependencies?

Here's a concrete example you can use for illustrative purposes: Can I set node_modules/bar to target "es2022" and still use it in the Electron project that targets "es6"?

As an aside: Is it advantageous to list more entries in lib?


Solution

  • The target setting determines how JS language constructs (not standard JS APIs) get downleveled. I.e. syntactic sugar from newer JS language standards can be downleveled, but in general, the TS compiler will not / cannot downlevel usages of JS APIs which do not exist in older ECMA Script standards (ex. Promise.all, Array.prototype.includes).

    The lib setting determines what version of the standard JS APIs / builtin libraries (such as the DOM) should be expected to be available on the environment on which the code runs. I.e. JS APIs which are in the chosen standard will have their typings made available, and usages of those that are not available should trigger compiler warnings.

    Note: you should not need to specify multiple ES libs at the same time. Just specifying the newest one you expect to be able to use should suffice, along with any desired subsections from newer versions (ex. es2015, es2017.String). By listing multiple, you are telling the TS compiler to pull into scope typings for multiple JS API versions. I can't think of anything bad that would happen as a result of that, but neither can I think of anything good that could happen from it, and I'd rather bet my money that someone can think of something bad (as opposed to good) that would happen.

    Each project/package is compiled from TS to JS on its own. When a project/package is compiled, there is no compilation of dependencies (which are compiled separately), and the TS compiler uses the settings in that project/package's tsconfig.json to compile it.

    Here are some implications:

    • A project should not expect things to just work if it depends on another package which emitted JS for a newer target than the one it chose

      • I.e. the dependency may use lanugage constructs that don't need to be downleveled for the target which it chose, but would have needed to be downleveled further for a dependent package which uses a lower target.
    • A project should not expect things to just work if it depends on another package which uses newer libs (i.e. newer standard JS APIs) than those that it chose.

      • I.e. the dependency may expect to run on an environment newer than the one the dependent expects to run on.

    In both cases, the problem would technically be a problem for the dependent, and would be observed as the dependency not working as expected or erring on certain older environments which the dependent intended to support. There are two approaches to avoid/mitigate this situation from arising:

    • On the dependent side (the one that uses dependencies):

      • Where possible, add polyfills for JS APIs which dependencies expect to be able to use but which the dependent doesn't expect to exist in the deploy-environment.
      • Tools can be inserted into the project's build-system which bundle the dependencies with the project's own code and do another pass of downleveling (in addition to the pass that the TS compiler does for each project separately according to each project's different tsconfig settings). Ex. with some WebPack + Babel setup.
    • On the library author side:

      • Many popular libraries which use TS source code choose conservative values of target and lib, such as ES6 (ES2015), which is very well supported at the time of this writing. They do this to increase the chance that their library can be compatible with a dependent. You can thank them for that.

    I.e. Foo's usage of Bar is okay (both target and lib are higher in Foo than Bar), but the Electron App's usage of Foo is not.

    I don't think the TS compiler will warn you if you make blunders when it comes to this, since after compilation, tsconfigs don't usually get distributed with packages. I.e. the TS compiler has no way to get that information about the package. But as already discussed, you should not. There's probably room for improvement in this area in the TS tooling and ecosystem, but for some reason, the current state of affairs is seen as normal/acceptable- Perhaps because many popular libraries make an effort to avoid language features and JS APIs which don't yet have very wide support.