TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Maintaining Emitted Backwards Compatibility Across Minor Releases

Open JasonGore opened this issue 2 years ago • 17 comments

Bug Report

🔎 Search Terms

maintain emitted backwards compatibility ts minbar across minor releases

🕗 Version & Regression Information

  • This changed between versions TS4.4 and TS4.5

⏯ Playground Link

Playground link with relevant code showing d.ts emitted inline type imports that break consumers < TS4.5

💻 Code

import { type DOMElement } from "react";

export type SomeElement = DOMElement;

🙁 Actual behavior

Downstream consumers using < TS4.5 break upon d.ts output containing inline type imports. downlevel-dts does not appear to support making inline type imports backwards compatible.

🙂 Expected behavior

I wanted to create this issue primarily for collaboration, but I'm creating it as a bug as I think at the very least there is a gap in downlevel-dts.

TS regularly adds new features like inline type imports that make emitted TS output incompatible for any downstream consumer using any older version of TS. TypeScript considers these features opt-in and therefore aren't listed as a breaking change. However, since devs don't easily know which minor release features break backwards compatibility, it becomes untenable to use any new feature introduced by any TS minor release for fear of breaking any downstream consumer that isn't at the latest possible version. This ends up introducing significant friction in maintaining TS update velocity, either because we don't want to upgrade as frequently or because we introduce issues using new TS features that only become known as downstream consumers use our emitted source.

While there are tools like downlevel-dts it doesn't appear to cover this specific inline type import issue, so that wouldn't fix our issue in this case.

It seems there should be some test suite in TS that determines whether the emitted output of each new minor release feature is backwards compatible. At the very least, this could feed into downlevel-dts to uncover gaps in the downlevel tooling. It would also be really helpful if this could feed into a minbar configuration that would indicate to devs whether or not the features they are using break backwards compatibility. If I could tell TS which TS version I want to maintain compatibility with and have it fail compilation if the emitted output is not compatible, that would be ideal.

To summarize, as things currently stand, our options are:

  • Allow devs to use new features and respond reactively to downstream issues as they occur. This is our current mode of operation and is untenable because it causes significant friction with our releases and our downstream consumers.
  • Stop updating to minor releases altogether. If using any new minor feature can break any of our downstream consumers using any earlier version of the same major release, our only realistic alternative may be to avoid using any new minor release features entirely.
  • Some way of understanding how using any features in a given release affect TS minbar compatibility. It'd be nice if TS could take in a minbar target configuration and fail compilation if that minbar is violated with the use of a feature, such as inline type imports.
  • Find some method to validate feature compatibility that feeds into downlevel-dts to uncover gaps such as inline type imports not being transformed.

JasonGore avatar Nov 02 '22 22:11 JasonGore

My statement that downlevel-dts doesn't support inline types was based on a conversation I had with Daniel, but it seems like that maybe we were wrong. I still think the primary point stands as we'd rather maintain backwards compatibility by disallowing feature use rather than by making use of downlevel-dts.

JasonGore avatar Nov 02 '22 22:11 JasonGore

mark

Czhang0727 avatar Nov 02 '22 23:11 Czhang0727

I would also +1 to Jason's remarks above - adding features to d.ts files in minor releases forces producers to never minor bump typescript. This is incredibly unfortunate because we want to be on the latest version at all times, especially for producer code, where newer versions might catch new bugs sooner.

It would be really ideal to consider d.ts typing output to produce new syntax only in a major release. Add a feature? major release. Don't want to major release yet? Find a way to represent the syntax in the current major release grammar and emit that. In the case of inline type in the original example, the 4.x typescript d.ts should just split out type imports into separate lines, making it compatible with 3.7 and above. The compiler can still handle inline types in a minor release, just not emit typing output that's incompatible with the major release snapshot.

I'd also love to see a comment emitted in typings indicating the typings version. For example, something like:

// TypeScript compatibility: 4.0.0

At a glance, any producer could know exactly what minimum typescript version is expected. In the example above I know if I am using any version of TS 4.x or above, my compiler will accept the syntax. It might even be useful metadata for the compiler, because now there is enough information to have a better error than "syntax error". Example:

Error: The package "[email protected]" has typings targeting a minimum version
of TypeScript 4.5. You are using 3.8, and unrecognized grammar in the
typings was encountered:

import { type Bar } from 'bar';
         ^^^^

dzearing avatar Nov 03 '22 04:11 dzearing

I think this is effectively a duplicate of https://github.com/microsoft/TypeScript/issues/36207.

https://github.com/microsoft/TypeScript/issues/36207#issuecomment-574921540

We'd need some very strong evidence that downlevel-dts is insufficient to solve the use case. Already you could run your source code through downlevel-dts and error if there are any deltas that result from running the tool.

fatcerberus avatar Nov 03 '22 06:11 fatcerberus

Since "minor release" is often mentioned here, I'd like to add something. TypeScript does not follow semantic versioning. The version increment is always the same: Minor until 9, then major. There's no deeper meaning behind it, like "no breaking change".

Every TypeScript release should be treated as a major release, because that's what it is.

MartinJohns avatar Nov 03 '22 11:11 MartinJohns

Since "minor release" is often mentioned here, I'd like to add something. TypeScript does not follow semantic versioning. The version increment is always the same: Minor until 9, then major. There's no deeper meaning behind it, like "no breaking change".

Every TypeScript release should be treated as a major release, because that's what it is.

The TS team has also repeatedly asked us to use new versions as quickly as possible. Having a major breaking release every few months is a tax that very few large projects can afford, particularly when it spreads downstream across the whole org and quickly spirals out of control. This just effectively means people will upgrade TS far less frequently, and that is a problem that also compounds.

We'd need some very strong evidence that downlevel-dts is insufficient to solve the use case. Already you could run your source code through downlevel-dts and error if there are any deltas that result from running the tool.

Related to above, these replies are basically justifying TS breaking releases every few months because of a secondary optional compiler that is available. Our repo has literally hundreds of packages that compile in TS, and that's just in our intermediary repo. Adding what is effectively another compiler has significant CI performance and maintenance costs, and that's not even counting whether or not the downlevel compiler is guaranteed 100% compatible and supported as well as TS itself. In our case, it'd be much more preferable to maintain type compatibility by disallowing features that break it.

JasonGore avatar Nov 03 '22 16:11 JasonGore

these replies are basically justifying TS breaking releases every few months because of a secondary optional compiler that is available

The comment I quoted was from the team lead himself so I’m not the person you have to convince.

fatcerberus avatar Nov 03 '22 16:11 fatcerberus

Find some method to validate feature compatibility

Sure -- previous TypeScript versions are available on npm, and you can install+run them to see if they "work" given whatever definition of "work" is operative in your environment.

Regarding semver, even under a broad definition of what constitutes a breaking change, this isn't one. It's allowed to add new features in minor releases, by definition. It doesn't make any sense to construe this as a breaking change because prior versions of the software can't consume code written using that new feature.

We've been quite careful to make it so that declaration emit, whenever possible, doesn't rely on new features unless you used those new features in your code.

In our case, it'd be much more preferable to maintain type compatibility by disallowing features that break it.

This is a great opportunity for a lint rule.

Since you're both internal I'd invite you to stop by office hours today and we can chat more.

RyanCavanaugh avatar Nov 03 '22 16:11 RyanCavanaugh

Sure -- previous TypeScript versions are available on npm, and you can install+run them to see if they "work" given whatever definition of "work" is operative in your environment.

There is nothing particularly niche or customized about our environment to justify us taking on this tax. We are merely consumers of a TypeScript environment in a broad ecosystem of upstream and downstream TypeScript consumers, all who adopt new versions at their own cadence.

Regarding semver, even under a broad definition of what constitutes a breaking change, this isn't one. It's allowed to add new features in minor releases, by definition. It doesn't make any sense to construe this as a breaking change because prior versions of the software can't consume code written using that new feature.

We could have a discussion about the semantics of semver, but I think it avoids discussing the practical implications of this approach which I've touched on above. The fact is, as a large TS project in an ecosystem of other TS repos and projects, there is significant friction in adopting new TS versions, regardless of semver semantics.

This is a great opportunity for a lint rule.

I worry again this is a tax we are being asked to pay simply for being relatively nimble in adopting new versions. I'm not even against it outright, but as I alluded to in my post, we're not sure which features cause problems to write eslint rules against. Ultimately that just means we don't adopt TS and use any new features at all. Knowing which features to avoid would be a significantly valuable first step.

JasonGore avatar Nov 03 '22 16:11 JasonGore

I think it'd be valuable for us to more explicitly call out in the release notes which new syntaxes will appear in .d.ts files if you use them. What folks do with that information is up to them, but we should be clearer about it.

RyanCavanaugh avatar Nov 03 '22 16:11 RyanCavanaugh

I think extra release notes are great but it's ignoring the fact that if a producer upgrades ts and considers using said new feature, they now have mental math to jump through. When was this feature introduced? What is this particular package's minimum typescript requirement for consumers? What versions are our consumers using? When new PRs come through especially for OSS projects, we also have this mental math in reviewing changes. When consumers complain that we merged some PR that broke them, do we feel bad about ourselves, that we missed this? Do we blame the consumer for not being on the latest typescript? Do we blame the typescript team? You can see this spiraling into chaos when you consider the scale at which we are using TypeScript across producer and consumer boundaries.

We recognize downlevel-dts is an option to work around the chaos. It costs not just engineering work to integrate, but CI build time which is a HUGE problem at scale. It would be desirable to not add another serial build step requirement, another AST parse, to every package in the world that uses typescript. Plus there are concerns that it's not in the typescript repo, isn't releasing on the same cadence which might lead to it falling behind (not saying it is, just saying it would be better to be releasing as a ts feature.)

Lint rules are another way for us to safe guard against feature usage, but it's a HUGE tax, and we're losing the benefit of upgrading at all with this! We want to use inline types. We don't want to drag our feet on an upgrade. We don't want to upgrade and write lint rules to avoid using the new features of the upgrade. That makes no sense.

A better approach would be: keep adding new syntaxes. Keep progressing. Add release notes to describe the new features. But either save the new volatile d.ts emissions for a major release, or even have a tsconfig setting that says to run downlevel-dts under the hood of typescript to emit compatible typings, but give us something that we can guarantee we aren't breaking partners by using the language. This would likely mean you could avoid an ast parse, and downlevel-dts behavior would be a first class well maintained citizen supported in the same release pipeline.

dzearing avatar Nov 03 '22 17:11 dzearing

A better approach would be: keep adding new syntaxes. Keep progressing. Add release notes to describe the new features. But either save the new volatile d.ts emissions for a major release

I don't understand what you're proposing here. Let's say we add some new type feature, like kinda, which has its own new semantics that aren't equivalent to anything currently in TS. You write

export const m: kinda boolean = probably;

In the .d.ts file we should then do what?

export const m: /* what? */

RyanCavanaugh avatar Nov 03 '22 17:11 RyanCavanaugh

Maybe worth noting that the mentality of “extra tools are a tax, this would be better built-in” is a potential slippery slope w.r.t. where to draw the line, and being an all-in-one tool is explicitly a non-goal for TS:

https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

[Don’t be] an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.

fatcerberus avatar Nov 03 '22 17:11 fatcerberus

In the .d.ts file we should then do what?

Good question! What would downlevel-dts do? I think that's the answer. I'm guessing that it would translate to any or boolean | any or something, I don't know. But not everyone wants that behavior, that's pretty clear.

Moving the downlevel-dts logic into the tsc process and/or having a policy of "don't change the d.ts grammar in a major release" are examples of the forcing factor we want to have to avoid these breaks and to force these questions. We want these questions to be considered; not just assumed "hey new d.ts feature, upgrade and deal with it! or don't upgrade... or maybe use this other tool that might (or probably does not) deal with this feature"

So my first thought would be: move downlevel-dts into tsc and consider these questions and how it should respond.

My second thought is that it should be configurable. Maybe tsconfig could have a setting for what sort of typings to emit, something like typesCompatibility:

  • baseline - e.g. snap to 3.4 or 3.7 as a baseline. Anyone using tsc 3.4+ can parse this grammar
  • current - (default I think) Anyone using tsc of the same major release that i'm using can parse this grammar
  • next - the new features are emitted and require a minimum of {major.minor.patch} version of tsc to parse.

This is just an idea.

dzearing avatar Nov 03 '22 17:11 dzearing

Maybe worth noting that the mentality of “extra tools are a tax, this would be better built-in” is a potential slippery slope w.r.t. where to draw the line, and being an all-in-one tool is explicitly a non-goal for TS

This is a fair point and indeed a slippery slope. I think downlevel-dts has a good reason to just ship with tsc though, beyond the extra reparse costs. We want the tranform to be inline with the new features. Something like: add a new feature that breaks typings parsing? Cool, you need to consider how this gets downleveled to maintain compatibility.

dzearing avatar Nov 03 '22 18:11 dzearing

We want these questions to be considered; not just assumed "hey new d.ts feature, upgrade and deal with it! or don't upgrade... or maybe use this other tool that might (or probably does not) deal with this feature"

I mean, we did consider this, and wrote downlevel-dts as a response. The solution you're eschewing is the one we chose to provide. It's not integrated into tsc for a variety of reasons:

  • It can iterate faster because it's not tied to any TS release (and iterate slower when nothing changed)
  • A very valid way to use it is to produce a "latest" .d.ts file, then run downlevel-dts multiple times to produce multiple downlevel targets. You can do this in parallel in your build system and it will be faster than having multiple tsc invocations (no matter how trivial)
  • All the "what does old stuff do" logic is moved into a single codebase

etc.

If CI speed is the most important thing, which it sounds like it is, then downlevel-dts is the fastest thing you can do here since it's parallelizable in the task of producing multiple downlevel targets (which sounds like an important scenario for your build). What other factors are involved?

RyanCavanaugh avatar Nov 03 '22 18:11 RyanCavanaugh

If CI speed is the most important thing, which it sounds like it is, then downlevel-dts is the fastest thing you can do here since it's parallelizable in the task of producing multiple downlevel targets (which sounds like an important scenario for your build). What other factors are involved?

Even if done in parallel, there is still a CI cost in terms of compute time.

My primary concern in adopting downlevel-dts is making sure that it isn't a second-class citizen, has guaranteed compatibility, and receives support at the same priority as TS itself. Is that the case?

I think your idea of explicitly call out in the release notes which new syntaxes will appear in .d.ts files if you use them is a good one. I'm curious if that could tie into a tsconfig option (as David mentioned) that would help us avoid breaking features entirely. We don't necessarily need emit-compatible source that download-dts provides, particularly if that will add significant maintenance and support friction. Some validation or minbar enforcement via a tsconfig would be amazing on its own.

JasonGore avatar Nov 03 '22 18:11 JasonGore

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

typescript-bot avatar Nov 19 '22 18:11 typescript-bot

Good question! What would downlevel-dts do?

That's an even better question. I think downlevel-dts would become abandonware, to be honest.

dejayc avatar Oct 19 '23 01:10 dejayc