microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.08k stars 12.5k forks source link

Skip typechecking; only emit (support `--transpileOnly` in `tsc`, re-open of #4176) #29651

Closed dtinth closed 4 months ago

dtinth commented 5 years ago

Search Terms

isolatedModules, incremental build slow, allowJs, transpileOnly. #4176

Suggestion

Support a compilation mode where files are only transpiled without typechecking. This can greatly improve compilation speed. Similar to the transpileOnly flag in ts-node and ts-loader.

Use Cases

At @taskworld, we are trying to migrate our project to TypeScript. We have 1400 source files.

As we try to get our .js files to be processed and transpiled by tsc, setting "allowJs": true makes tsc take a painfully long time (40 seconds) to complete, even in --watch mode. tsc --diagnostics shows that lots of time is spent in typechecking phase.

I have checked these issues:

I tried to profile the tsc process, and found that a lot of time is spent in resolveCallSignature.

If we can skip the type-checking process, this compilation phase should be faster.

This seems to be supported in both ts-node and ts-loader, since TypeScript provides the “single-module transpilation mode” (the ts.transpileModule API). So, I looked for a way to do it using tsc. Turns out, it is not available, and we have to somehow use the ts.transpileModule API directly.

https://github.com/Microsoft/TypeScript/issues/4176#issuecomment-128505179

A fancier solution would be to use the compiler's transpile API directly.

https://github.com/Microsoft/TypeScript/issues/13538#issuecomment-273695261

If you are willing to get your entire project compiling under the isolatedModules switch, then you can safely wire up your build system to do a simple emit of only changed files, which should be practically instant, followed by a re-typecheck.

Examples

All evidence so far suggests that we have to build our own tooling which behaves like babel -d build-dir source-dir (e.g. compiles each file separately) but for TypeScript. And so we implemented our own workaround:

// tsc-fast.js
const args = require('yargs')
  .options({
    force: {
      alias: 'f',
      description: 'Recompiles even if output file is newer.',
      type: 'boolean',
    },
    watch: {
      alias: 'w',
      description: 'Watches for file changes.',
      type: 'boolean',
    },
  })
  .strict()
  .help()
  .parse()

const watch = require('gulp-watch')
const ts = require('gulp-typescript')
const newer = require('gulp-newer')
const tsProject = ts.createProject('tsconfig.json', {
  isolatedModules: true,
})
const vfs = require('vinyl-fs')
const debug = require('gulp-debug')
const sourcemaps = require('gulp-sourcemaps')

function main() {
  let compiling = false
  let pending = false

  function compile() {
    if (compiling) {
      pending = true
      return
    }
    compiling = true
    const rawInput = tsProject.src()
    const input = args.force
      ? rawInput
      : rawInput.pipe(
          newer({
            dest: 'dist',
            map: f => f.replace(/\.ts$/, '.js'),
          })
        )
    input
      .pipe(sourcemaps.init())
      .pipe(tsProject())
      .pipe(sourcemaps.write('.'))
      .on('error', () => {
        /* Ignore compiler errors */
      })
      .pipe(debug({ title: 'tsc:' }))
      .pipe(vfs.dest('dist'))
      .on('end', () => {
        compiling = false
        if (pending) {
          pending = false
          compile()
        }
      })
  }

  compile()
  if (args.watch) {
    watch(['app/**/*.js', 'app/**/*.ts', '!app/vpc-admin/front/**/*'], compile)
  }
}

main()

To typecheck in separate step, we simply run tsc --noEmit in a separate CI job. Also, VS Code takes care of typechecking in the editor, so we already get instant feedback for type errors.

Checklist

My suggestion meets these guidelines:

dtinth commented 5 years ago

I noticed the questions in #30117 and would like to give some answers for context:

Is this just piping errors to /dev/null ?

If this has to do with .on('error', () => { /* Ignore compiler errors */ }), this only prevents the script from crashing Node.js when a compiler error occurs. gulp-typescript outputs error messages to console already.

Why is this user's project so slow?

I realize that I haven’t put the diagnostic information in the issue.

This is the diagnostic info for incremental compilation.

Files:           2099
Lines:         967290
Nodes:        2998862
Identifiers:   983959
Symbols:      1266945
Types:         211713
Memory used: 1473430K
I/O read:       0.01s
I/O write:      0.98s
Parse time:     0.57s
Bind time:      0.02s
Check time:    16.95s
Emit time:      7.99s
Total time:    25.54s

Here’s the corresponding CPU profile:

image

As you can see, about 80% of “Check time” is spent in checkCallExpression function. So maybe there might be some slow stuff going on there — I may have to do a more thorough investigation.

Is this a global project or a module project?

This is a module project, but allowJs is on and most of JS files are CommonJS modules.

(cc: @RyanCavanaugh)

RyanCavanaugh commented 5 years ago

Discussed. We felt like we had a good handle on this and then totally deadlocked on the question of whether this mode would imply --noResolve:

Separately, the problem that this mode only "works" if --isolatedModules is on was a point against it.

Ultimately a tool that just calls ts.transpileModule on some defined set of files is quite trivial to write, and would have clearer semantics than what we might provide out of the box.

We were curious if things got better for you with --incremental.

biels commented 5 years ago

I would add another reason c) treat all ts errors as warnings and still emit the results. This is available on browser projects when using webpack with ts-loader using transpileOnly: true.

The advantage of this workflow is that it enables faster prototyping when combined with hot reload / run on save. You can see type errors alongside runtime errors and get the best of both worlds. You still care about ts errors as they show up in your IDE and build logs but they do not necessarily break your build.

This may not be desired on all projects or environments but that's where the flag comes in. I would suggest to add a transpileOnly boolean flag to the compliler so that it supports this behavior natively and can be used for projects that do not target the browser.

ayroblu commented 5 years ago

tsc can emit even if there are errors (how we quickly fix bugs by trying things) Looking at this cause --incremental doesn't re emit files that haven't changed, nor does it remove files that have been deleted (which is why I want this) ts-node already has a fast flag, so it'd be nice to have this here

ayroblu commented 5 years ago

Discovered typescript-transpile-only guess this solves my problems: https://github.com/cspotcode/typescript-transpile-only

biels commented 5 years ago

This should be included in tsc as --transpileOnly flag

On Mon, Jun 24, 2019, 07:50 Ben Lu notifications@github.com wrote:

Discovered typescript-transpile-only guess this solves my problems: https://github.com/cspotcode/typescript-transpile-only

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/microsoft/TypeScript/issues/29651?email_source=notifications&email_token=ABHLQMG5MAGNM2PWKEJD7PDP4BOBJA5CNFSM4GTLYFDKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODYL2RAA#issuecomment-504866944, or mute the thread https://github.com/notifications/unsubscribe-auth/ABHLQMC5IOHUUBHISY3BKBLP4BOBJANCNFSM4GTLYFDA .

canvural commented 5 years ago

Another reason this would be great is compiling in the production environment. In production we don't install dev dependencies, so running tsc on production results in an error because it can't find type definitions. But our code is checked before it reaches to production, so in production, we know its fine type wise.

sheerun commented 5 years ago

My use case is quickly transiling typescript snippet so I can use it in non-typescript project. Currently I need to use online playground for this...

kitsonk commented 5 years ago

There is feedback for Deno that having a transpile only option might be a viable way to speed up startup of some workloads (denoland/deno#3321) While we cache the output of a compile, if you are loading some known good TypeScript for the first time, just parsing and emitting, and not type checking, might be a good option.

weswigham commented 5 years ago

@kitsonk if you have code you assume is "known good", why not add some more into that definition of "known good", rather than just "was type checked", and support shipping and loading a bytecode? Kinda like .pyc files for python.

kitsonk commented 5 years ago

@weswigham that is something @ry and I talked about, V8 does have a byte code type format that we could cache instead of even JavaScript. But let's say you were consuming a source module that your instance of Deno hasn't cached yet. You would go to a well maintained source, that you know us valid and injest it, and all you want out is the "bytecode" as fast as you can. Personally I wouldn't want to do it that way, I would always want to spend a few cycles checking the code, but I guess I can see an "just erase the types" emit only as supporting some types of workloads.

weswigham commented 5 years ago

🤷‍♂ Deno uses the typescript API, right? It just needs to use ts.transpile in that scenario, then.

kitsonk commented 5 years ago

@weswigham 😊 um... yeah... 🤦‍♂ sorry for the noise.

beenotung commented 4 years ago

@canvural my workaround is to compile the project locally than rsync the dist to the server

nicojs commented 4 years ago

I've got 2 more use cases. One has to do with performance in a CI/CD pipeline, the other has to do with mutation testing.

CI/CD

Let's say your CI/CD pipeline looks like this:

                                    +--------+
                               +--->+  test  |
+-------------+    +--------+  |    +--------+
| npm install +--->+ tsc -b +--+
+-------------+    +--------+  |    +--------+
                               +--->+  lint  |
                                    +--------+

When tsc -b here takes minutes it might make sense to split type checking off into a separate task like so:

                                                    +-----------------+
                                                +-->+  test           |
                                                |   +-----------------+
                                                |
+-------------+   +------------------------+    |   +-----------------+
| npm install +-->+ tsc -b --transpileOnly +------->+  lint           |
+-------------+   +------------------------+    |   +-----------------+
                                                |
                                                |   +-----------------+
                                                +-->+ tsc -b --noEmit |
                                                    +-----------------+

Depending on the duration caching between build jobs (gitlab ci does a good job at this) this saves minutes per pipeline.

Mutation testing

Let's say you're building a mutation testing framework that supports typescript (something like Stryker mutator, which I am maintainer of) and you're planning to implement mutation switching in order to improve performance (which we're planning to do). You would want to split of type checking and transpiling into separate steps.

Let's say you have this statement:

const foo = 'bar ' + (40 + 2);

The mutation testing framework might want to create 2 mutants here:

// mutant 1
const foo = 'bar ' - (40 + 2);
// mutant 2
const foo = 'bar ' + (40 - 2);

Note that mutant 1 results in a compile error, since in TypeScript - isn't defined for strings.

When using mutation switching, the mutation testing framework will make 1 version of the code that contains both mutants. Like so:

let foo;
switch(global.activeMutant) { 
  case 0:
    // mutant 1
    foo = 'bar ' - (40 + 2);
    break;
  case 1:
    // mutant 2
    foo = 'bar ' + (40 - 2);
    break;
}

The mutation testing framework will now test both mutants. First it will type check (using the type checker api) and if valid, run tests for that mutant.

The model of transpiling once and in the background type checking all mutants individually saves a lot of time over compiling each mutant indivually, since the test runner process will not have to load in new files at all.

Ultimately a tool that just calls ts.transpileModule on some defined set of files is quite trivial to write, and would have clearer semantics than what we might provide out of the box.

Writing such a tool might not take that much time, but writing and maintaining a drop in replacement will take a lot of time. Just think about the CLI changes between releases. A daunting task if you ask me.

askirmas commented 4 years ago

It was my first time of writing babel config. So babel.config.json:

{
  "presets": [
    ["@babel/preset-env",
      {
        "targets": {
          "node": 10
        }
      }
    ]
  ],
  "plugins": [
    "@babel/plugin-transform-typescript"
  ]
}

execution:

babel-node --extensions .ts -- index.ts
askirmas commented 4 years ago

I didn't check yet ts-node --transpile-only

nicojs commented 4 years ago

I found a workaround that might work for some use cases.

It works by prefixing every file content with // @ts-nocheck. It only works from TS3.7, since that version introduced // @ts-nocheck. It uses the createSolutionBuilder api, so works with project references 👍

// my-little-builder.js
const compiler = ts.createSolutionBuilder(ts.createSolutionBuilderHost(
  {
    ...ts.sys,
    readFile(fileName, encoding = 'utf8') {
      if (fs.existsSync(fileName)) {
        let content = fs.readFileSync(fileName, encoding);
        if (!fileName.includes('node_modules') && fileName.endsWith('.ts')) {
          content = '// @ts-nocheck\n' + content;
        }
        return content;
      }
    }
  },
  ts.createEmitAndSemanticDiagnosticsBuilderProgram,
  (d) => console.log(ts.formatDiagnosticsWithColorAndContext([d], {
    getCanonicalFileName: fileName => fileName,
    getCurrentDirectory: process.cwd,
    getNewLine: () => os.EOL
  })),
  (status) => console.log(`status: ${status.messageText}`),
  (summary) => console.log(`build summary: ${summary}`)
), ['tsconfig.json'], {});
const exitStatus = compiler.build();
console.log(`Exit status: ${exitStatus}`);

@RyanCavanaugh What do you think of allowing users to set a global @ts-nocheck option? Maybe by adding --no-check? People would be allowed to override it per file. This would be inline with the way type checking for js files works. It would probably need a warning in the console, since type checking would be off.

Note: This does not allow for notable performance improvement:

$ time npx tsc -b

real    0m55.219s
user    0m0.106s
sys     0m0.246s

$ npm run clean
$ time node my-little-builder.js
build summary: 0
Exit status: 0

real    0m54.140s
user    0m0.015s
sys     0m0.031s
dsagal commented 4 years ago

Since this is "awaiting more feedback". In watch mode, I ideally want to have my output ASAP and my errors whenever they are ready. We used to use ts-loader with fork-ts-checker-webpack-plugin, which together provided exactly that. We switched to tsc --build, and now lost that significant speed-up.

If --transpileOnly option were present, we could in theory run in parallel one --watch build with --transpileOnly, and another with --noEmit (to just report type errors) to get the best of both worlds. But even better would be a --watch-fast option which does both together (produce the transpiled output quickly, then run the type-checking to report any errors).

hanayashiki commented 3 years ago

your script save my day! i'd like if you publish that to npm.

meadowsys commented 3 years ago

Another use case:

Currently I have my project setup with tsconfig.cjs.json, tsconfig.mjs.json, tsconfig.types.json, and they all extend from tsconfig.base.json with base options. As expected, cjs produces cjs, mjs produces mjs output, etc. I have these scripts:

{
   "scripts": {
      // build just runs types, cjs, and mjs in order
      "build": "npm run clean && npm run build:types && npm run build:cjs && npm run build:mjs",
      "build:types": "tsc --project tsconfig.types.json",
      "build:cjs": "tsc --project tsconfig.cjs.json",
      "build:mjs": "tsc --project tsconfig.mjs.json",
   }
}

I want it to be where it only type checks when I'm building types (turn on transpileOnly in the other tsconfig files), because it could save a lot of time when building and publishing to npm. Everything would stop if types fails, so its equally as safe

ahamid commented 3 years ago

There are some projects which will never completely typecheck. The current state of jsdoc/typedoc tools appears to be that they do not support non-typechecking projects, and without the ability to transpile simply to preserve source comments this prevents documentation generation. That's a step backwards.

vegerot commented 3 years ago

I'm a few years late here. What is the best option if I want to skip typechecking when running tsc?

hanseltime commented 3 years ago

Hey all,

I stumbled upon @nicojs's answer a while back and finally decided to add a tool as a package that can allow me to update files with the @ts-nocheck comment.

The script I have published is a cli and programmatic file rewriter that does not have the built-in compilation script.

If you find yourself having to use non-compliant typescript projects (like in an enterprise setting where type-checking is not enforced), hopefully this can help your workflow!

https://www.npmjs.com/package/ignore-bad-ts

oleksandr-danylchenko commented 2 years ago

Any updates on how we can ignore @types from the devDependencies (which are pruned on installation) and just transpile the code?

beenotung commented 2 years ago

@oleksandr-danylchenko I'm not sure about your question, maybe the skipLibCheck flag is what you're looking for?

vitorgouveia commented 2 years ago

@oleksandr-danylchenko I'm not sure about your question, maybe the skipLibCheck flag is what you're looking for?

i have this problem also, when in production, only the dependencies will install, the devDependencies (which include all @types) will not, this causes compilation error in tsc, because it cannot find declaration files for the dependencies

for this case, something like babel or sucrase is needed, so the code is just compiled and not type checked skipLibCheck did not work for me

beenotung commented 2 years ago

i have this problem also, when in production, only the dependencies will install, the devDependencies (which include all @types) will not, this causes compilation error in tsc, because it cannot find declaration files for the dependencies

I suggest you transpile the project from typescript to javascript in development environment or CICD environment, then deploy the javascript on production server. This way you don't need typescript on production server.

trivikr commented 2 years ago

Currently I have my project setup with tsconfig.cjs.json, tsconfig.mjs.json, tsconfig.types.json, and they all extend from tsconfig.base.json with base options. As expected, cjs produces cjs, mjs produces mjs output, etc. I have these scripts

I want it to be where it only type checks when I'm building types (turn on transpileOnly in the other tsconfig files), because it could save a lot of time when building and publishing to npm. Everything would stop if types fails, so its equally as safe

We have a similar setup in AWS SDK for JavaScript (v3) and an option to skip type checks while compiling non-types artifacts (CJS/ESM) will help us improve build speeds.

vitorgouveia commented 2 years ago

skipDefaultLibCheck to true solved my problems, it just skipped all the default typescript type checking and transpiled my files.

So useful for npm ci

trivikr commented 2 years ago

skipLibCheck, or deprecated skipDefaultLibCheck only skips type checks on dependencies and not in the application code as per the description of the configuration.

beenotung commented 2 years ago

If you want to transpile the typescript project into javascript without type checking, you can use esbuild, it runs very fast. You may reference to this setup: https://github.com/beenotung/base32/blob/master/scripts/esbuild.js

Fonger commented 2 years ago

We output multiple targets: commonjs and esmodule I think only one of them should do type-checking.

beenotung commented 2 years ago

When I build multi-target project, I use esbuild to build js in common js, esm, and browser bundle. Then use tsc to build .d.ts file (only the last step has type checking). With run-p in npm-run-all, the two build steps can be run in parallel.

When I build node.js-only project, I use tsc to build js and .d.ts in one pass.

DerGernTod commented 2 years ago

Another use case:

Currently I have my project setup with tsconfig.cjs.json, tsconfig.mjs.json, tsconfig.types.json, and they all extend from tsconfig.base.json with base options. As expected, cjs produces cjs, mjs produces mjs output, etc. I have these scripts:

{
   "scripts": {
      // build just runs types, cjs, and mjs in order
      "build": "npm run clean && npm run build:types && npm run build:cjs && npm run build:mjs",
      "build:types": "tsc --project tsconfig.types.json",
      "build:cjs": "tsc --project tsconfig.cjs.json",
      "build:mjs": "tsc --project tsconfig.mjs.json",
   }
}

I want it to be where it only type checks when I'm building types (turn on transpileOnly in the other tsconfig files), because it could save a lot of time when building and publishing to npm. Everything would stop if types fails, so its equally as safe

any updates on this? there's plenty of tools that can do this in the meantime, but i'd prefer as little dependencies as possible. this here is basically the same use case as i have multiple times in my projects. would really be very convenient

tycrek commented 2 years ago

I'm a few years late here. What is the best option if I want to skip typechecking when running tsc?

3 days shy of a year later, do we have an answer yet?

y-nk commented 1 year ago

@tycrek https://github.com/microsoft/TypeScript/issues/29651#issuecomment-958648333

hanseltime commented 1 year ago

Just saw @y-nk 's comment. I would like to note that the package I wrote takes advantage of the node_modules folder. I've been doing a lot more with yarn pnp recently and think there would need to be some additional lift for that. I don't currently have the time to add that configuration but anyone who would like to play with that need, can.

beenotung commented 1 year ago

I published a cli package live-tsc that do transpiling without type checking.

It is a recursive wrapper built on top of esbuild, with support on watch mode and adding custom post-hook.

martinjlowm commented 1 year ago

We use direct Git repo references for our packages and we ended up with the following ESBuild script for emitting CJS, ESM and declaration files upon package installation (postinstall) (inspired by @beenotung's script):

#!/usr/bin/env node

const esbuild = require('esbuild');
const { nodeExternalsPlugin } = require('esbuild-node-externals');
const tsPaths = require('esbuild-ts-paths');
const { globPlugin } = require('esbuild-plugin-glob');
const { dtsPlugin } = require("esbuild-plugin-d.ts");

Promise.all([
  esbuild.build({
    entryPoints: ['./src/**/*.ts', './src/**/*.tsx'],
    outdir: 'dist',
    bundle: false,
    minify: true,
    format: 'cjs',
    platform: 'node',
    sourcemap: true,
    sourcesContent: false,
    jsx: 'automatic',
    target: 'node12',
    plugins: [dtsPlugin(), globPlugin(), tsPaths('./tsconfig.json'), nodeExternalsPlugin()],
  }),
  esbuild.build({
    entryPoints: ['./src/**/*.ts', './src/**/*.tsx'],
    outdir: 'dist/es',
    bundle: false,
    minify: true,
    format: 'esm',
    platform: 'node',
    jsx: 'automatic',
    sourcemap: true,
    sourcesContent: false,
    target: 'node14',
    plugins: [dtsPlugin(), globPlugin(), tsPaths('./tsconfig.json'), nodeExternalsPlugin()],
  }),
]).catch((error) => {
  console.error(error);
  process.exit(1);
});

Obviously, it adds a bit of overhead to have esbuild-* packages as production dependencies - but it definitely beats having to store dist files in source control. Hope somebody finds it useful :)

thorsent commented 1 year ago

Use case: trying to compile some random repo that has out of date typedefs. Not my code, and I just need to transpile and run the thing to get a result. I frankly had no idea up until this minute that transpileOnly was ts-node! I had assumed all along that it was a tsc feature.

bschlenk commented 9 months ago

Maybe another use case. My company uses a monorepo with pnpm. All our pnpm workspaces are written in typescript, and we have to build them before they'll work when installed in other packages. We know all these packages were already typechecked in ci. What I want to do is build with esbuild, and generate .d.ts files with tsc. I'm wondering if skipping the type checking here would speed up the .d.ts file generation any significant amount.

acutmore commented 9 months ago

I'm wondering if skipping the type checking here would speed up the .d.ts file generation any significant amount.

@bschlenk it sounds like you would be interested in #47947, which enables .d.ts generation without type checking.

inoyakaigor commented 6 months ago

One more use case Running tests in built-in Nodejs test runner.

All my tests has written in TS and obviously Nodejs can't execute it without precompilation into JS. I can do this in this way:

node --import ./node_modules/typescript/bin/tsc --test ./src/**/*.test.ts",

My code is not perfect and contain a bunch of type errors and it is OKAY. For typeschecking I have another solution so in my tests I would test only my code not types. For that it would be great if there exists CLI command like --transpileOnly

jakebailey commented 4 months ago

TypeScript 5.5 added --noCheck; is there any other work to be done?

DanielRosenwasser commented 4 months ago

I believe it's not user-facing in 5.5, even on the API level. https://github.com/microsoft/TypeScript/pull/58839 makes it broadly accessible though, so we can mark this as fixed.

jakebailey commented 4 months ago

Oops, typo, I meant 5.6!

Yegorich555 commented 1 month ago

Well... --noCheck is console-argument. How to use it with ts.createWatchCompilerHost(...) where I need to set ts.CompilerOptions? Any thoughts? Expected that in options I could point noCheck into compiler options directly too... image