Search code examples
javascripttypescriptcompatibility

How can I make a 4.X Typescript project compatible with an older version of Typescript like 3.X?


How can I make a package built on top of TS 4.X compatible with 3.X? For example, if I have a newer version, use new features, otherwise use any or unknown or whatever is supported in older version.

Is there any possibility to use directives for that purpose?


Solution

  • Preface

    This answer assumes the question is about taking source code (.ts) of a project written using TypeScript 4.x constructs and making type declaration files (.d.ts) emitted from them compatible with a TypeScript 3.x compiler for the benefit of the users of the package- as opposed to writing source code that uses 4.x constructs and somehow making it compatible with a 3.x compiler (the way the question is phrased is ambiguous with respect to this). I make this assumption because:

    • If you decide you want to use language features in your source code that aren't supported by older compiler versions, you are (whether you realize it or not) making a decision to drop support for building the project using those older compiler version. If you have a strong reason to want to support building the project using those older compilers, then I'm pretty sure you just have to not use those newer language features (or somehow convince the maintainers of the compiler to backport those changes to older compiler versions, which I think is pretty rare).

    • I'm not personally aware of any reasons not to upgrade your compiler version unless you have very strict security policies and require your dependencies and build tooling to be audited. I'd wager that that's pretty rare in the JavaScript development scene where the landscape is known for changing rapidly.

    downlevel-dts

    Note: TypeScript has a feature to "downlevel" the JavaScript it emits to convert language constructs from later versions of the ECMA Script standard to constructs that work in older versions. (see the compileOptions.target field of tsconfig).

    As far as I know, TypeScript itself doesn't have such a feature to downlevel the typings files it emits (including triple-slash-directives at the time of this writing), but Nathan Sanders (a maintainer of Definitely Typed) maintains an open-source project, downlevel-dts, to downlevel .d.ts files which can downlevel typings all the way down to typescript v3.4 syntax.

    package.json.typesVersions and semver-ts.org

    The "'Downleveling' Types" section of semver-ts.org explains how you can script downleveling types for each typescript version where new non-backwards-compatible language constructs were introduced and how to tell a package-user's compiler which version of the types to use and where to find them:

    When a new version of TypeScript includes a backwards-incompatible change to emitted type definitions, as they did in 3.7, the strategy of changing the types directly may not work. However, it is still possible to provide backwards-compatible types, using the combination of downlevel-dts and typesVersions. (In some cases, this may also require some manual tweaking of types, but this should be rare for most packages.)

    • The downlevel-dts tool allows you to take a .d.ts file which is not valid for an earlier version of TypeScript (e.g. the changes to class field emit mentioned in Breaking Changes), and emit a version which is compatible with that version. It supports targeting all TypeScript versions later than 3.4.

    • TypeScript supports using the typesVersions key in a package.json file to specify a specific set of type definitions (which may consist of one or more .d.ts files) which correspond to a specific TypeScript version.

    The recommended flow would be as follows:

    To avoid copying too much from off-site material (plagiarism), I'll summarize the steps in my own words (go read the source for the full steps with examples):

    1. Install downlevel-dts as a dev-dependency (and some other helper tools).
    2. Call downlevel-dts to downlevel types to whichever older type declaration versions you want to support (this can be scripted).
    3. Update your package.json to register call your script after generating types for the more recent type declaration verion.
    4. Register your generated older-version type declaration files in your package.json file using the typesVersions field.
    5. Make sure the generated files are included with your package's files (update the files field, or whichever similar fields you are using).

    limitations of downlevel-dts

    Note that there are limitations. The following is a quote from downlevel-dts's readme:

    Note that not all features can be downlevelled. For example, TypeScript 4.0 allows spreading multiple tuple type variables, at any position in a tuple. This is not allowed in previous versions, but has no obvious downlevel emit, so downlevel-dts doesn't attempt to do anything. Be sure to test the output of downlevel-dts with the appropriate version of TypeScript.

    Problematic aspects of other proposed solutions:

    • "Use an older typescript version to emit your typings"

      • This assumes the asker of the question is using the TS compiler to emit typings from .ts files and doesn't work if they are maintaining them by hand, which is often the case for very large projects that were first written in JS and don't have the bandwidth to migrate to TS.
      • This probably requires you to make your .ts source code to not use TypeScript language constructs that were introduced in a version newer than the compiler that you have to use to emit typings in the TypeScript language version you want to emit. This is not ideal because it may be much cleaner to write code using newer language constructs, or impossible to do something with just older constructs.
    • "Maintain typings for both TypeScript language version"

      • This seems to make the opposite assumption: That the project maintains manual typings files for manually written JS code instead of being transpiled to .js and emitting .d.ts from .ts files.
      • This is a big maintenance burden (it's already a big maintenance burden to manually maintain .d.ts files!). It may be acceptable for small projects, but not for libraries with a large or complicated API surface.