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

Do not check in the transpileModule API #50699

Closed
5 tasks done
rricard opened this issue Sep 9, 2022 · 10 comments
Closed
5 tasks done

Do not check in the transpileModule API #50699

rricard opened this issue Sep 9, 2022 · 10 comments
Assignees
Labels
API Relates to the public API for TypeScript Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@rricard
Copy link

rricard commented Sep 9, 2022

Suggestion

🔍 Search Terms

  • transpileModule
  • skip typechecking
  • no typechecking

Semi-related issues found:

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Modify the transpileModule API to mostly do the following:

  • Parses a source string
  • Runs the corresponding transforms to get to the target ES version
  • Emits a resulting string with a sourceMap string

The API itself would be unchanged but would be guaranteed to not hit the type-checker if a certain set of compatible compiler options is set. (deoptimizing options TBD)

Achieving this without modifying TypeScript is currently not possible with the current TypeScript API: there is currently no way to just apply the transforms without running the checker as a Program has to be created in the process.

Alternatively, if changing transpileModule is not possible we would be interested in exposing more minimal API primitives so we could build up this API ourselves:

  • A public Printer API that can issue sourcemaps
  • A way to run transforms on TS ASTs without the need to build up a Program (and run the checker...)

📃 Motivating Example

In our build system, for certain workflows, we introduced a new build mode that runs transpileModule instead of compiling and type-checking the full project. transpileModule is run on a per-file basis as we are reading files into our build stream, meaning there are no inter-file ordering dependencies.

Approximative `transpileModule` API usage
const result = ts.transpileModule(tsFileStr, {
    compilerOptions, // <- with an ESNext target
    reportDiagnostics: true,
    moduleName: tsFilePath,
    fileName: tsFilePath,
});
if (result.diagnostics) {
    // report syntax errors
    if (result.diagnostics.length > 0) {
        return;
    }
}
const jsFilePath = tsFilePath.replace(/\.tsx?$/, ".js");
const jsFileStr = result.outputText;
const sourceMapPath = jsFilePath + ".map";
const sourceMapStr = result.sourceMapText;
// ...

With this new mode, we managed to get 4x faster than an equivalent fully-typechecked build! This is an amazing speedup that is appreciated for running common workflows (while VSCode continues to type-check things in the editor).

We are still trying to get faster: most of the time is now spent type-checking inside transpileModule, as seen in the CPU flamecharts.

Screenshot 2022-09-08 at 13 47 40

We researched using alternative build tools such as swc or esbuild but using their transpile API alone in our setup is not significantly better than using transpileModule! (I could do a writeup on this if anyone is interested in that research) However, those other tools do tend to get really fast when they are run standalone and handle file I/O but unfortunately this does not fit our use case.

At this point, a pure js-based transform might be the better option and simply using the TypeScript compiler infrastructure would make sense since all the tools to create a relatively fast module type-stripping system should be there but are not exposed through the current API.

💻 Use Cases

Our goal is to be able to keep transpileModule and have it go faster if the options don't require checking:

// ✅ doesn't require checking, fast path:
ts.transpileModule(tsFileStr, {
    compilerOptions: {
        target: "esnext",
    },
});

// ❌ requires checking, slow path:
ts.transpileModule("class c { @x f: string };", {
    compilerOptions: {
        target: "esnext",
        emitDecoratorMetadata: true,
        noEmitHelpers: true,
    },
});
// emits with some type system info:
class c { f }
__decorate([], x, __metadata("design:type", String), c.prototype, "f", void 0);

By avoiding type-checking code paths when possible (deoptimizing options TBD), we could speed things up significantly again for the users of transpileModule.

In the future, it could also be the base to then introduce a tsc flag for fast unchecked builds as suggested in #29651.This would permit tsc to be significantly faster if opted into with the flag.

Deoptimizing options

This is a temporary list that we intend to build up over time:

  • emitDecoratorMetadata
  • ... TBD ...

If you can provide some guidance on whether this would be a good thing to change/fix, along with any implementation constraints/tips, I would be happy to attempt an implementation.

@johnnyreilly
Copy link

I could do a writeup on this if anyone is interested in that research

Me. I'm interested in that research!

@rricard
Copy link
Author

rricard commented Sep 9, 2022

I think I want to have the whole story before going there so I'd like to try the proposed optimizations here and compare them. Also to be clear, it was within our very specific setup. When using swc/esbuild "as intended" the results are different!

@fatcerberus
Copy link

It was my understanding that transpileModule already didn't do typechecking? If you wanted typechecking you had to go through the whole rigamarole of creating a Program, etc.

@rricard
Copy link
Author

rricard commented Sep 9, 2022

Well transpileModule is mostly a wrapper around creating a Program to compile a single file. The checking happens and is relatively fast compared to a multi-file project but is ignored as diagnostics around checking are not reported.

@andrewbranch andrewbranch added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Sep 9, 2022
@andrewbranch
Copy link
Member

Paging @DanielRosenwasser for thoughts

@DanielRosenwasser
Copy link
Member

I believe this is because the checker is doing this for grammar checks - but the cost comes from the walk, plus the fact that some checking necessarily happens from the checker's walk.

The upside of this is that we have parent pointers (which makes things easy for us), and it reduces time-to-interactive in editor situations (since it's not part of the up-front always-on cost of parsing/binding).

The downside is that it's still a separate tree walk and there's some incidental type-checking, and that adds up.

I think @sandersn's work on #45349 is also relevant - we've spoken about whether or not grammar checks could be folded into a lighter grammar walk that the full checker can periodically jump into - or possibly brought back into the parser/binder.

@andrewbranch
Copy link
Member

My guess is that if someone spent a day or two trying to optimize and eliminate work from transpileModule, they would find some fairly low-hanging fruit, and some fruit not reachable without an architectural overhaul.

@DarrenDanielDay
Copy link

I think it's a good idea since type checking for complicated types can be costly, but there are some TypeScript features that require type information to emit JavaScript code, like const enum NotAnObject, import { SomeType } from "some-module";.

For example the following TypeScript code

const enum NotAnObject {
  Foo = 1,
}
console.log(NotAnObject.Foo);

will be transpiled into JavaScript code:

"use strict";
console.log(1 /* NotAnObject.Foo */);

TypeScript needs to know NotAnObject is not a reference to an object (unlike normal enum objects), and NotAnObject.Foo should be transpiled to the enum value 1. But without type information of the whole project, TypeScript cannot find how NotAnObject is defined -- it can be a global declaration in other files of the project, although I think it's not good coding style. And import { SomeType } from 'some-module' without declaring type only import like import { type SomeType } from 'some-module' or import type { SomeType } from 'some-module' also requires type information to know those named imports should not be emitted in the JavaScript code, otherwise the JavaScript code may not work correctly (as ES Module).

In other words, does this API implicitly require isolatedModules to be configured to true?

@andrewbranch
Copy link
Member

Yes, isolatedModule means transpiler-friendly. Any single-file transpiler, including our transpileModule API, may produce broken code if the program doesn’t check without errors with isolatedModules enabled.

@acutmore
Copy link
Contributor

acutmore commented Jun 7, 2024

I think this is now fixed by #58364. Thanks @weswigham!

@DanielRosenwasser DanielRosenwasser added Fixed A PR has been merged for this issue API Relates to the public API for TypeScript and removed In Discussion Not yet reached consensus labels Jun 7, 2024
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 5.5.1 milestone Jun 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API Relates to the public API for TypeScript Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants