Deploying TypeScript: recent advances and possible future directions

[2025-04-13] dev, typescript
(Ad, please don’t block)

In this blog post we look at:

  • The current best practice for deploying library packages: .js, .js.map, .d.ts, .d.ts.map, .ts
  • Recent new developments in compiling and deploying TypeScript: type stripping, isolated declarations, JSR, etc.
  • What the future of deploying TypeScript might look like: type stripping in browsers, etc.

The traditional way of deploying TS  

Currently, the TypeScript compiler tsc performs three tasks:

  1. Type checking
  2. Emitting JavaScript files
  3. Emitting declaration files

To make all of that happen, tsc needs to be configured via a tsconfig.json file which influences type checking and the emitted JavaScript code. As a result, there are many different “dialects” of TypeScript, depending on how strict type checking is – as specified in tsconfig.json.

For a library file util.ts, we get the best development experience if we deploy the following files:

  • util.js: the runtime functionality
  • util.js.map: a source map that maps source code locations in util.js to source code locations in util.ts. That enables functionality such as showing stack traces for util.ts while running util.js and showing .ts code while debugging .js code.
  • util.d.ts: the types
  • util.d.ts.map: a declaration map that maps source code locations in util.d.ts to source code locations in util.ts. That enables functionality such as going to the definition of a type.
  • util.ts: included because it is the target of the source map and the declaration map.

Except for util.ts, it helps if all files sit next to each other: Then we only have to point the package exports to util.js and TypeScript will automatically find the other files.

Recent advances  

Emitting JavaScript and declarations syntactically: type stripping and isolated declarations  

Part of the reason why tsc is relatively slow is that it parses the TypeScript syntax and then has to perform costly semantic analyses to emit JavaScript and declarations:

  • JavaScript:
    • To remove type-only imports, tsc has to figure out which constructs are JavaScript and which constructs are type-related.
    • Transpiling newer JavaScript to older JavaScript also makes compilation more complicated.
  • Declarations: tsc has to perform type analyses because there may be inferred types: The types of variables, the return types of functions, etc.

Recent advances are about emitting both kinds of code via purely syntactic analyses – by forcing us to write simpler TypeScript that does not require semantic analyses:

  • Type stripping imposes the following restrictions:
    • We must mark type-only imports with the keyword type.
    • We can’t use non-JavaScript runtime features that require transpilation such as enums and JSX.
    • We can’t transpile new JavaScript to old JavaScript.
  • Isolated declarations impose the following restrictions:
    • The return types of exported functions must be specified explicitly.
    • The types of exported variables must either be specified explicitly or trivial to infer: Inferring string from a string literal is acceptable, inferring a type from a function call is not.

Note that we don’t need a tsconfig.json if we emit TypeScript via type stripping and declarations via isolated declarations. We only need it for type checking. That clarifies its role.

All major server runtimes now run TypeScript directly  

The following platforms let us run TypeScript directly:

  • Deno has supported it for a long time.
  • Bun supports it.
  • Node.js supports type stripping (where some TypeScript features, such as enums and JSX, are not allowed) by default and more language features via a CLI option.

JSR (JavaScript Registry): a registry for JavaScript and TypeScript  

JSR lets us upload packages with TypeScript files:

  • TypeScript-first platforms such as Deno install TypeScript files.
  • On other platforms, .js files and .d.ts files are generated on demand – which is fast because type stripping and a technique called no slow types (which is similar to isolated declarations) are used.

The pros and cons of .d.ts  

Points in favor of .d.ts:

  • They make type-checking faster: When importing from a dependency:

    • With a .d.ts file, there is less code to parse: only the types of exported constructs.
    • .ts files require type checking to extract the types.

    Andrew Branch has benchmarked the difference and type-checking the d.ts version of a dependency took 58% of type-checking the .ts version.

  • .d.ts files are more stable than .ts files. This is an example given by Daniel Rosenwasser: “when TypeScript introduced the satisfies operator, library authors were able to use it without worrying about consumers because expression-level syntax never appears in .d.ts files.”

Source:

What’s next?  

Should packages contain TypeScript?  

Node.js does not support TypeScript in packages, JSR supports publishing packages with .ts files. Both have pros and cons. A few thoughts:

  • The syntactic stability of .d.ts is an interesting point in its favor.
  • As far as I can tell, type-checking a .ts dependency could be sped up if it adheres to the constraints of isolated declarations: Compared to a .d.ts dependency, there would still be more parsing. But no semantic analysis would be needed because the .d.ts information is syntactically embedded inside the .ts file.
  • Compiling .ts on demand to .js and .d.ts has become fast, thanks to type stripping and isolated declarations. If that is done, it should probably happen on the server. Otherwise, local package manager binaries would become outdated too quickly.

Type stripping in browsers  

ES Module Shims brings new ESM features to older browsers. It lets us run TypeScript in browsers – via on-demand type stripping:

<script type="module" src="app.ts"></script>

It’s interesting how ES Module Shims handles MIME types (source):

  • Browsers ignore filename extensions of JavaScript files and only look at MIME types.
  • Many servers automatically use the MIME type video/mp2t for files with the filename extension .ts. Therefore, ES Module Shims does not check the mime type if a file has the filename extension .ts or .mts. For all other files, MIME types are required and the mime type application/typescript is supported.

ECMAScript proposal: Type Annotations  

A proposal to add optional type annotations to JavaScript (by Gil Tayar, Daniel Rosenwasser, Romulo Cintra, Rob Palmer) is currently at stage 1.

The core idea of the proposal is:

  • People can add type annotations and type declarations to JavaScript with syntax rules that allow a variety of notations to be used: TypeScript, Flow, etc.
  • JavaScript engines ignore those type annotations and declarations and run the code as if they weren’t there (i.e., they perform type stripping).
  • Various type checkers (TypeScript, Flow, Hegel, Ezno, etc.) can be used to statically analyze code during development.

Most server-side runtimes already directly support TypeScript, so what’s a better solution for browsers: type-stripped TypeScript or JavaScript with optional type annotations?

  • JavaScript with annotations will probably always be more syntactically limited.
  • It’s interesting that the forgiving rules of JavaScript type annotations partially shield us from syntactic changes in TypeScript.

What is the future of JSX?  

With type stripping – will JSX become less popular? There are alternatives that use tagged templates – e.g.:

What is the future of enums?  

Type stripping does not allow enums either. Two alternatives are currently on the horizon:

  • The proposal for ECMAScript enums has recently gotten new attention and one of its goals is providing an upgrade path from TypeScript enums:
    enum Color {
      Red = Symbol('Red'),
      Green = Symbol('Green'),
      Blue = Symbol('Blue'),
    }
    
  • A pull request for erasable enum annotations provides better types when an object literal is used as an enum:
    const Color: enum = {
      Red: Symbol('Red'),
      Green: Symbol('Green'),
      Blue: Symbol('Blue'),
    };
    

Further reading