Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managing code compatibility in a TypeScript ecosystem with multiple compiler versions #25778

Closed
igrayson opened this issue Jul 18, 2018 · 6 comments
Labels
Duplicate An existing issue was already created

Comments

@igrayson
Copy link

igrayson commented Jul 18, 2018

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.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 18, 2018

you might find the proposal in #22605 sufficient for your needs.

the assumption in #22605, that you have a "tool" that can emit a .d.ts compatible with different versions of the compiler.

@mhegazy mhegazy added the Duplicate An existing issue was already created label Jul 18, 2018
@igrayson
Copy link
Author

igrayson commented Jul 18, 2018

Thanks @mhegazy. Yes, I should have clarified that I'm interested in recommendations that are possible with today's tools.

Adapting #22605 into such a thing, I believe it looks like this?

  1. some-typescript-lib compiles a .d.ts with tsc@2.9.2.
  2. For each release, developer hand-modifies output .d.ts into multiple backwards-compatible flavors index-2.8.d.ts, etc.
    a. Alternatively: enhance tsc to be able to emit downlevel'd .d.ts.
    b. Alternatively: build a tool that will downlevel source .ts, which input into older tsc versions, "handling" modern language features with no downlevel equivalent.
  3. some-typescript-lib has a postinstall script that detects the consumer's tsc version, picks the right flavor, and swaps out (overwrites) ./index.d.ts.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 18, 2018

We have not had explicit guidance on this regard. what we have seen ppl do are either one of two approaches, 1. option a above, where the whole org moves their TS version all at once. 2. for libraries to test against older versions, and try to isolate the breaks from their users. Most of the breaks do not escape to the .d.ts files (with few exceptions).

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@cordiosd
Copy link

Backward/Forward .d.ts compatibility system #31907

@benevbright
Copy link

@igrayson what’s your final solution now?
Btw, thanks for all the detail.

kinyoklion pushed a commit to launchdarkly/js-core that referenced this issue Jan 16, 2024
**Requirements**

- [ ] ~I have added test coverage for new or changed functionality~
- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
- [ ] ~I have validated my changes against all supported platform
versions~

No new functionality has been added (only types import/export
refactoring).

**Related issues**

#345
#347

**Describe the solution you've provided**

Remove the usage of [type modifiers on import
names](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#type-modifiers-on-import-names)
to improve backwards compatibility with early versions of TypeScript.

**Describe alternatives you've considered**

The obvious solution would be upgrading the TypeScript version in the
consumer, but this is not always easy (it's not in our case). [More info
here](microsoft/TypeScript#25778).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants