Description
We're looking for recommendations from the TypeScript team on the best way (with today's tools) to mitigate the organization-level developer pain of a large internal ecosystem with multiple TS versions, and whether you know of any existing approaches. I'm especially interested to hear stories of how other teams have concretely mitigated it (I link to one such below, from Google).
This isn't a TypeScript bug; I'm aware of #14116 and am not pushing for a fix from within TypeScript.
The pain
We have a growing number of TypeScript services that depend on a growing number of internal TypeScript libraries, and developers commonly hit against compilation failures caused by a mismatch in compiler versions between a consuming service or library and one of its (potentially transitive) dependency libraries.
While these pains are equally true for public and internal libraries, we're primarily interested in what we can do with the packages we have direct control over (i.e., we can mostly guarantee semver is followed).
We use npm (and yarn) for dependency management, we generally follow semver internally, and we haven't realized a perfect way to apply these tools towards the problem.
Problematic scenarios:
1. Consumer's compiler is older than dependency's compiler
This is the natural case where the dependency's compiler produced .d.ts
code with language features that the consumer's compiler doesn't understaned.
Usually, the resolution has been to upgrade the consumer's compiler.
2. Consumer's compiler is newer than dependency's compiler
Less obviously, this can also cause failure. Example: TypeScript 2.9 widened the type of the keyof
operator, rendering some .d.ts
declaration code emitted by TypeScript 2.8 unimportable by a 2.9 compiler.
Usually, the resolution has been to downgrade the consumer's compiler.
Solutions considered
In both scenarios above, the common resolution (upgrade/downgrade) is often complicated by the graph of mismatched compiler versions among all of the consumer's transitive dependencies -- a problem we anticipate to get worse as more libraries are built.
a. Enforce a single TypeScript X.Y
version across the organization
Using an npm postinstall script (or other), assert that every service and library builds with a single blessed X.Y
version of TypeScript.
Google does this (video), and they have some advantages that makes this realistic (a monorepo; centralized dev resources to grind out compiler upgrades).
Pros
- Guarantees
.d.ts
compatibility, between internal packages.
Cons
- The cost of upgrading the blessed version organization-wide could be prohibitively high. We may never upgrade TS again (or undo enforcement to do so), because the cost scales linearly with the number of libraries we use.
b. Bump library major versions for TypeScript upgrades
When a library upgrades the version of TypeScript used to build, also bump its major version. This doesn't fully solve the problem, but it reduces the chance that a consumer will break from a minor version update of a dependency. Breakage can still happen if the consumer/dependency are on different TS versions which are compatible for the subset of language features used in the dependency's old .d.ts
files, but incompatible for language features in an interface added by a minor version update.
Pros
- Cheap guard rail that will usually prevent package minor version upgrades from breaking consumers.
Cons
- Doesn't improve the story for upgrading compiler versions of consumer packages.
This also doesn't provide developers with any guidance on how to navigate major version upgrades for their dependencies -- other than "update the package; run tsc; hope nothing explodes".
c. Libraries declare compatible consuming TypeScript compiler versions
Using a package.json
key, have each library declare what TS versions it believes its emitted .d.ts
files can be imported by. A postinstall
script throws if consumers are running an unsupported version (problem: without a yarn/npm lockfile, postinstall script can't be sure which TS compiler is actually in play)
At its simplest (package.json
key manually updated by developers) it's error-prone, but over time converges on useful guidance.
Pros
- Gives "fast fail" feedback for developers adding or upgrading libraries with known compiler version incompatibility.
Cons
- If declarations are manual: error-prone; will miss incompatibilities until after first breakage.
- If declarations are automatic: involves some fairly complex scripting.
- Doesn't improve the story for upgrading consumer package compiler versions (i.e., how to discover and )
Both (b) and (c) still leave service developers with the challenge of detecting the highest safe version of TS for their service, and library developers with the risk that they'll need support multiple branches for consumers with (very) different TS versions.