Open-source project
nonara/ts-patch avatar
nonara/ts-patch

ts-patch: custom transformers inside the TypeScript compiler

Augment the TypeScript compiler to support extended functionality

954 stars32 forksTypeScriptMIT

At a glance

What is it?
ts-patch extends the TypeScript compiler so plugins can transform sources during the build, either through a live compiler that patches on the fly or through a persistent patch written into node_modules, with a narrowing of that second route on TypeScript 6 and a migration path from ttypescript.
Who is it for?
Adopt ts-patch if you need compiler plugins in a TypeScript project, whether for path alias resolution inside transformers, declaration file transforms, program level rewriting or diagnostic adjustment, since the live compiler route needs no changes to node_modules and works with tsc, ts-node, webpack and ts-jest, and the repository is not archived with a last push on 2026-09-09.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 45 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why the stock compiler ignores your transformers

TypeScript has no built-in way to run custom transformers during a build. ts-patch exists to make it usable from a build.

The README's one line description is that it patches typescript to allow custom transformers, plugins during build, and the important detail is where the plugins are declared. They are specified in tsconfig.json, or provided programmatically in CompilerOptions. So the plugins are declared in the compiler's own configuration, and ts-patch makes them take effect during the build.

The feature list gives the range. It can patch typescript through on-the-fly in-memory compiler routes or through persistent patches for supported files. It can patch supported compiler libraries, with the list of what can be patched available from the command itself via ts-patch /?. It can hook the build process by transforming the Program, which is the object created during ts.createProgram(), and it can add, remove or modify diagnostics, which is a capability that goes beyond what a normal transformer does.

The audience is therefore teams doing compiler-level work rather than application code: teams that resolve path aliases inside generated code, teams that transform declaration files, teams that need to rewrite a program before emit, and anyone who needs to suppress or add a diagnostic.

Compatibility with the older ttypescript package is stated as complete, and the README points at the live compiler method as the easy migration route.

Live compiler routes and the identity handed to transformers

Method 1 is the live compiler, which patches on the fly each time it is run, leaving node_modules untouched.

From the command line you use tspc instead of tsc. For tools such as ts-node, webpack and ts-jest, you specify the compiler as ts-patch/compiler. Beyond that there are additional routes for tools that need a specific TypeScript entry, and the README tabulates what each one supplies:

| Route | Library identity supplied to transformers | |-------|-------------------------------------------| | `ts-patch/compiler` | `typescript` | | `ts-patch/compiler/typescript` | `typescript` | | `ts-patch/compiler/tsc` | `tsc` | | `ts-patch/compiler/tsserver` | `tsserver` | | `ts-patch/compiler/tsserverlibrary` | `tsserverlibrary` |

That table is the part to read carefully. A transformer often branches on which library it is running under, and the live route lets you pick the identity deliberately. A tool that imports the compiler API directly gets typescript, the tsc entry gets tsc, and the server entries get tsserver and tsserverlibrary.

The mechanism is visible in the package's exports map, which points ./compiler and ./compiler/typescript at the same file, dist/compiler/typescript.js, and then has separate entries for dist/compiler/tsc.js, dist/compiler/tsserver.js and dist/compiler/tsserverlibrary.js. One patched implementation, several identities, and the toolchain picks the entry it needs.

Persistent patching narrowed to two files on TypeScript 6

Method 2 is the persistent patch, which modifies supported TypeScript files inside node_modules. This is the traditional approach and it still works, but the README is blunt about how much less of it there is now.

Persistent patching is still available for TypeScript 6, but the patchable surface is narrower than in earlier TypeScript versions. The reason is structural. TypeScript 6 ships thin entry shims for several library files, and some service entries delegate to shared implementation files, with tsserverlibrary.js delegating to typescript.js as the example. Rewriting those service files in place would not preserve enough entry context to distinguish direct compiler API use from tsc, tsserver or tsserverlibrary use.

So the commands survive but their reach shrinks:

shell
ts-patch install
ts-patch uninstall

install and uninstall now affect only typescript.js and tsc.js. The lower-level commands are limited to the same two targets:

shell
ts-patch patch typescript tsc
ts-patch unpatch typescript tsc

And tsserver.js and tsserverlibrary.js are not persistent patch targets on TypeScript 6, where the live routes are the way to make sure transformers still receive the correct library identity for those service entries.

This is a real migration cost for teams that patched their server entries in place. The functional outcome is the same, but the mechanism moves from editing files to selecting an import path.

Declaring plugins in the tsconfig plugins array

Configuration lives in the compilerOptions plugins array in tsconfig.json, and the README's example shows the shapes you can mix in one file:

jsonc
{
    "compilerOptions": {
        "plugins": [
            // Source Transformers
            { "transform": "transformer-module" },
            { "transform": "transformer2", "extraOption": 123 },
            { "transform": "trans-with-mapping", "resolvePathAliases": true },
            { "transform": "esm-transformer.mjs" },

            // Program Transformer
            { "transform": "transformer-module5", "transformProgram": true }
        ]
    }
}

Each entry names a module or a path, and anything beyond the known keys is passed straight through to the transformer as a custom option. The transformer file can be TypeScript or JavaScript.

The option table sets out the rest. transform is the only required key. after applies the transformer after the stock TypeScript transformers, and afterDeclarations applies it to declaration files. transformProgram is the switch that turns an entry into a Program transformer, which is the hook that runs during ts.createProgram(). resolvePathAliases resolves path aliases inside the transformer and needs the tsconfig-paths package. type selects the transformer entry point and defaults to program. import names the exported function and defaults to the default export. tsConfig points at a tsconfig.json for the transformer itself, which is how a transformer gets its own compile options and path mapping.

That last option is the one people miss. A transformer usually needs its own path resolution, and tsConfig is how you give it one.

Source transformers versus Program transformers

Two transformer kinds appear in the README, and the difference is about when they run and what they can reach.

A source transformer receives the compiler's own view of a file and returns a modified one. That is the classic shape, and it is where path alias resolution, custom syntax handling or rewriting of emitted declarations belongs.

A Program transformer runs against the Program object during ts.createProgram(), so it sees the whole program at once rather than one file at a time. The README lists transforming the Program as a separate feature from running a transformer, and the configuration table documents the transformProgram boolean that turns an entry into one.

The distinction has a practical consequence for ordering. A source transformer operates within the emit pipeline and is subject to the compiler's own sequencing, while a Program transformer can affect the program as a whole before sources are processed. If you need to add files, change the set of roots or influence the program's structure, the Program hook is the only one of the two that can do it.

Diagnostics are the third capability, listed separately: add, remove or modify diagnostics. That is not a transformer at all, it is a second kind of extension point, and it is the reason ts-patch is more than a transformer runner.

Version 4 also adds experimental support for ES Module based transformers, and the config example includes an esm-transformer.mjs entry.

Migrating from ttypescript and the removal of isEsm

Two migration notes deserve their own attention.

The first is ttypescript. The README states the client is fully compatible with legacy ttypescript projects and says migrating from ttypescript is easy, pointing at the live compiler method as the route. The practical reading is that the config surface carried over and the change is in how the compiler is launched: the live route or the tspc command.

The second is a removal. As of v4, isEsm has been removed, and the transformer module format is inferred from the file extension and package metadata instead. That is a behavioural change that will surface as a module resolution error rather than a silent difference, because the explicit switch that used to mark a transformer as ESM no longer exists.

The extension inference has a practical consequence. A transformer named with a.mjs extension is treated as an ES module, which is why the config example lists esm-transformer.mjs separately from the.js and.ts entries. The rule is the file extension plus the package metadata of the transformer package, so a transformer shipped in a package whose type field disagrees with the extension will resolve differently.

The source transformers are the recommended entry point for most use cases, and the README links to external notes on the compiler API for background on SourceFile and Program.

A major version that only works on TypeScript 6

The version policy is the deciding practical detail.

The README carries an important notice that ts-patch v4 supports TypeScript 6 and later only, and that TypeScript 5 projects should use ts-patch v3. That is a hard boundary rather than a deprecation warning. A team on TypeScript 5 cannot upgrade ts-patch without upgrading TypeScript, which in turn means evaluating the compiler changes on the other side of that upgrade.

The release history shows how often this happens in practice. Version 3.3.0 is dated 2024-12-04, 3.2.1 on 2024-06-26, and 4.0.1 on 2026-05-01. The 3.x line served the TypeScript 5 era, and the 4.x line targets the newer compiler. The repository is not archived, and the last push was on 2026-09-09, so the project is being worked on rather than parked.

For a maintainer, the cost of that churn shows in the repository layout. The build scripts compile two separate projects, projects/core and projects/patch, and the package.json marks itself private while the published artefact is assembled by a postbuild script at scripts/postbuild.js. Tests run under jest, with jest.config.js at the root and a perf script for measuring overhead.

The licence is MIT, recorded in the package metadata and in LICENSE.md at the repository root, which is permissive but still requires keeping the copyright notice.

Editorial conclusion

Adopt ts-patch if you need compiler plugins in a TypeScript project, whether for path alias resolution inside transformers, declaration file transforms, program level rewriting or diagnostic adjustment, since the live compiler route needs no changes to node_modules and works with tsc, ts-node, webpack and ts-jest, and the repository is not archived with a last push on 2026-09-09. Do not adopt v4 on a TypeScript 5 codebase, because the README states that v4 supports TypeScript 6 and later only and TypeScript 5 projects must stay on v3, and do not adopt the persistent patch if you rely on tsserver or tsserverlibrary, since those are no longer patch targets. Before you upgrade, verify two things: which entry point your toolchain actually loads, because ts-patch/compiler and ts-patch/compiler/tsc hand transformers different library identities, and whether your transformers rely on the isEsm option, which was removed in v4.

Frequently asked questions

How do I install ts-patch and use it?

Add it as a development dependency with <yarn|npm|pnpm> add -D ts-patch, then run tspc instead of tsc for the live compiler. For tools such as ts-node, webpack or ts-jest, point the compiler at ts-patch/compiler instead.

Which ts-patch compiler route should I use with ts-node or webpack?

Specify the compiler as ts-patch/compiler for those tools. The README tabulates the available routes: ts-patch/compiler and ts-patch/compiler/typescript supply the typescript identity, while ts-patch/compiler/tsc, /tsserver and /tsserverlibrary supply the matching service identity.

Can I still patch TypeScript persistently with ts-patch on TypeScript 6?

Yes, but with a narrower surface. install and uninstall now affect only typescript.js and tsc.js, and the lower-level patch and unpatch commands are limited to those two targets. tsserver.js and tsserverlibrary.js are not persistent patch targets on TypeScript 6, so the live routes should be used for them.

How do I declare a transformer in tsconfig.json for ts-patch?

Add entries to the plugins array in compilerOptions, each with a required transform key naming the module or path, and optional keys such as after, afterDeclarations, transformProgram, resolvePathAliases, type, import and tsConfig. Any other keys are passed to the transformer as custom options.

Which TypeScript versions does ts-patch v4 support?

The README states that ts-patch v4 supports TypeScript 6 and later only, and that TypeScript 5 projects should use ts-patch v3. The v3 line covers the earlier compiler, with 3.3.0 dated 2024-12-04.

What happened to the isEsm option in ts-patch?

It was removed as of v4. The transformer module format is now inferred from the file extension and package metadata, so an .mjs transformer is treated as an ES module without an explicit flag.

Official sources

  1. Issues
  2. License: MIT
  3. nonara/ts-patch on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nonara-ts-patch.svg)](https://hysenlabs.com/projects/nonara-ts-patch)

Was this useful?