Open arcanis opened 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.
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
?
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.
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.
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.
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 insideinclude
.📃 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?
💻 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).