question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC: allowing standalone `.d.ts` emit through explicit type annotations (--isolatedDeclarations, --noTypeInferenceOnExports?)

See original GitHub issue

Suggestion

TypeScript supports relying on type inference to produce (parts of) the API of a module. E.g. users can write (sorry, slightly contrived):

// in counter.ts:
import {Splitter} from 'textutils/splitter';
export function countParts(x: string) {
  return new Splitter(x).splitWords().size();
}

Note how you need to specify the type of x (otherwise it degenerates to any), but can leave out the return type of lengthOf if you like. TypeScript will infer the type, potentially using type information from the file’s (transitive) dependencies.

This causes two problems.

Readability. It is difficult to understand what the return type of countParts will be. This is purely a stylistic issue that could be fixed with a lint check (though there is some complexity e.g. due to typeof).

Compilation performance/parallelism.

Imagine you’re using project references, and you have a dependency structure:

app <-- counter <-- textutils/splitter

To compile, we need to first compile textutils/splitter, wait for that to complete, e.g. 5s, then compile counter, wait e.g. 3s, then compile app (6s). Total compilation wall time is |app| + |counter| + |textutils/splitter|, in our example 5s + 3s + 6s = 16 seconds.

Now assume we could produce .d.ts files without requiring transitive inputs. That’d mean we could, in parallel, produce the .d.ts files for textutils/splitter, counter (and app, though we don’t need that). After that, we could, in parallel, type check and compile textutils/splitter, counter, and app. Assuming sufficient available parallelism (which seems reasonable, given how common multicore CPUs are), total compilation wall time is the maximum time to extract .d.ts files, plus the time for the slowest compile. Assuming .d.ts extraction is purely syntactical, i.e. does not need type checking nor symbol resolution, it shouldn’t add more overhead than a few hundred ms. Under these assumptions, the wall time to wait for the project to compile would be 500 ms + 6s = 6.5 seconds, i.e. more than a x2 speedup.

The problem with that is that we cannot produce .d.ts files without running full type checking, I believe purely due to type inference.

RFC thus: I wonder if this would sufficiently motivate the ability to restrict using type inference in exported API?

E.g. we could have a noTypeInferenceOnExports compiler flag, that would allow TypeScript to parallelise emitting .d.ts across project references, and then parallelize type checking.

The counter point is that projects that experience slow builds in edit refresh situations might instead want to turn off type checking entirely for their emit, at least on critical paths. However that means users do not see compilation results, and produces additional complexity (e.g. when and how to report type checking failures).

Impact

We’ve run some statistics internally at Google on build operations.

As one would expect, this change has little impact on most incremental “hot inner loop” builds, as those typically just re-type check a single file and produce no .d.ts change at all, so caching saves us from long build chains. We’re seeing ~20% wall time improvements in the 90th percentile across all builds involving TypeScript.

However the impact on slower builds is more substantial. For a sample “large” project that sees both slow individual compiles and a long dependency chain, we see ~50% improvement in the 90th percentile, and 75% in the 99th percentile (which is representative of CI style “cold” builds with little caching).

🔍 Search Terms

performance compilation parallelism inference declarations

✅ 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.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:37
  • Comments:18 (9 by maintainers)

github_iconTop GitHub Comments

14reactions
RyanCavanaughcommented, Feb 23, 2022

I would frame it this way:

  • isolatedModules means a syntax-only tool can be guaranteed to do single-file transpilation correctly
  • isolatedDeclarations means a syntax-only tool can be guaranteed to do single-file .d.ts emit correctly

The potential perf gains here are indeed extremely large if we think about non-error-checking scenarios.

We’ll have to consider what the edge cases are where the syntactic rules might be sufficient to capture this invariant, but it’s a very interesting proposal.

12reactions
robpalmecommented, Feb 22, 2022

Thanks for raising this issue. This is a feature/area I had been considering prototyping for a while. It sounds like we’re thinking of the same thing but I’ll elaborate here so you can verify.

Feature Description

The key goal is to allow a declaration *.d.ts to be generated from a single *.ts file, without the need to examine the dependency tree. This implies that any types needed from dependencies will be imported by the generated *.d.ts. That’s a major change from today’s declaration generation that sometimes inlines (duplicates) those resolved types into the generated declaration rather than referencing them via imports.

This kind of standalone declaration generation isn’t possible for the full set of TypeScript source files today. An example problematic case is when an exported type is computed based on types originated in dependencies. Declaration files can’t express transitive concepts such as typeof a + typeof b so the emitted declaration is always a resolved type today.

import { a, b } from "./dependency";
export const sum = a + b;    // declaration emit will resolve this to a singular string or number

Potentially new declaration syntax could be added to mitigate this. But a general solution is to constrain the set of TypeScript that can be authored. Think of it as introducing stronger linting rules. When this need arose previously to permit standalone per-file TS->JS compilation, the isolatedModules option was introduced to constrain the source, forcing the user to write more explicit code in some cases. So I think the natural option name for this new constrained mode applied to declaration generation would be isolatedDeclarations

Pros & Cons

As you say, this feature will permit parallelisation of declaration emit. It will also reduce the blast radius of declaration regeneration needed when editing a single file. Another less obvious benefit is that it will improve type-checking performance for consumers of otherwise bloated declaration files - by eliminating the (increasingly rare) edge cases where excessive inlining super-sizes the declaration files. And when declaration emit is decoupled from type-checking, it opens the door for high-performance declaration emit tools in other languages, e.g. Rust/Go/Zig.

A downside of forcing isolation of declaration files is that it may increase the count of file accesses for anyone consuming that declaration file set during type-checking. Per-file overheads are noticeable particularly on Windows and particularly when malware scanners intercept the file access. This can be mitigated by declaration bundling.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Creating .d.ts Files from .js files - TypeScript
Run the TypeScript compiler to generate the corresponding d.ts files for JS files; (optional) Edit your package.json to reference the types. Adding TypeScript....
Read more >
lib.d.ts - TypeScript Deep Dive - Gitbook
A special declaration file lib.d.ts ships with every installation of TypeScript. This file contains the ambient declarations for various common JavaScript ...
Read more >
Should TypeScript Interfaces Be Defined in *.d.ts Files
I am going to follow this general rule: if I am creating an interface, then it should just be a normal *.ts file,...
Read more >
esbuild-plugin-d.ts - npm
TS. ESBuild plugin for compiling typescript declarations along with your outputted js. WARNING. This plugin was made to make it easier to build ......
Read more >
What Are *.d.ts files? How to Use *.d.ts Files in TypeScript?
How to use *. d. ts files.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found