microsoft / TypeScript

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

Dead code typecheck elimination #54826

Open arcanis opened 1 year ago

arcanis commented 1 year ago

Suggestion

🔍 Search Terms

dead code typecheck elimination

✅ Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

When the include field is set, TypeScript will only typecheck the given files ... and their dependencies, and transitive dependencies. It can grow unwieldy pretty fast.

It'd be good for performances to if TS could have a skipDeadCodeTypechecks call that would cause it to skip the typecheck of all statements that aren't part of the execution tree of the files listed inside include.

📃 Motivating Example

If you were announcing this feature in a blog post, what's a short explanation that shows a developer why this feature improves the language?

In the new TypeScript update, we're excited to introduce the skipDeadCodeTypechecks feature. This was born out of the need for a more efficient type-checking process. Previously, when setting the include field, TypeScript would typecheck all given files, their dependencies, and their transitive dependencies. This could quickly get out of hand, resulting in a typechecking process that would run over many more files than the author intended!

The skipDeadCodeTypechecks feature aims to solve this issue. Instead of typechecking every single file and their dependencies, this new feature will only typecheck the code that is actually executed when the files listed inside include are run. This means that TypeScript will skip typechecking for code that is not part of the execution tree for these files, hence "dead" code.

💻 Use Cases

We have a very large repository we're trying to split into reference workspaces. But until we get there, we'd like to selectively pick the folders to typecheck. We do that with the include field, but it's still very slow (30s to typecheck what's supposed to be a relatively small part of the application).

fatcerberus commented 1 year ago

My instinct is that most, if not all of the performance you’d gain from this would be lost to the additional static analysis required to determine which code is “live”. For example, control flow analysis for type narrowing doesn’t currently descend into function calls (see #9998); this would require doing so recursively.

RyanCavanaugh commented 1 year ago

I'm not even sure this is even remotely tractable in the easiest cases. Consider something like

class Animal {
  move() { }
}
class Fish extends Animal {
  move() { this.swim(); }
  swim() { } }
}
class Cat extends Animal {
  move() { this.prowl(); }
  prowl() { } }
}

const p: Animal = new Fish();
const g: Animal = new Cat();
moveMe(p);

function moveMe(x: Animal) {
  x.move();
}

How would we statically (i.e. not just running the code) figure out that Fish#swim is invoked, but Cat#prowl ?

arcanis commented 1 year ago

I think "dead code elimination" was perhaps a mistake, as it implies relying on the control flow, which isn't what I have in mind.

I was using "dead code elimination", but more in its abstract sense than a 1:1 mapping with runtime semantics. In your example, there is no "dead code" from a typecheck perspective - the whole unit of code must be typechecked, so there are no "dead types".

TypeScript needs some types in order to validate the code in include. It doesn't need the others (as in, it doesn't reference them at all, and neither do the transitive type dependencies). As a result, is it necessary to validate them all?

For example, if we assume that main.ts is the only file in the include array:

main.ts

import {type Foo} from './foo';
let foo: Foo = 42;

foo.ts

import type {Component} from './my-custom-ui-library-index';
export type Foo = number;
export type Bar = Component | Foo;

In that situation, why does TypeScript even follows the ./my-custom-ui-library-index resolution? Clearly, Component has no bearings on main.ts, so I don't really care whether it typechecks.

RyanCavanaugh commented 1 year ago

Clearly, Component has no bearings on main.ts

This isn't true. ./my-custom-ui-library-index, or a file it imports, could have a declare global { augmentation of any type and could cause virtually any other type operation to go in a different direction.

arcanis commented 1 year ago

That's the case as well for any other file in my repository that happens to not be imported by any of the files within include, but we live with that. What I suggest would have little functional difference (if this change is behind a flag, of course): files with global reach would have to be covered by include, which is strongly recommended anyway.