{"id":3748,"date":"2023-03-09T13:33:34","date_gmt":"2023-03-09T21:33:34","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/typescript\/?p=3748"},"modified":"2023-03-15T15:02:54","modified_gmt":"2023-03-15T23:02:54","slug":"typescripts-migration-to-modules","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/typescript\/typescripts-migration-to-modules\/","title":{"rendered":"TypeScript&#8217;s Migration to Modules"},"content":{"rendered":"<p>One of the most impactful things we&#8217;ve worked on in TypeScript 5.0 isn&#8217;t a feature, a bug fix, or a data structure optimization.\nInstead, it&#8217;s an infrastructure change.<\/p>\n<p>In TypeScript 5.0, we restructured our entire codebase to use ECMAScript modules, and switched to a newer emit target.<\/p>\n<h2>What to Know<\/h2>\n<p>Now, before we dive in, we want to set expectations.\nIt&#8217;s good to know what this does and doesn&#8217;t mean for TypeScript 5.0.<\/p>\n<p>As a general user of TypeScript, you&#8217;ll need to be running Node.js 12 at a minimum.\n<code>npm install<\/code>s should go a little faster and take up less space, since the <code>typescript<\/code> package size should be reduced by about 46%.\nRunning TypeScript will get a nice bit faster &#8211; typically cutting down build times of anywhere between 10%-25%.<\/p>\n<p>As an API consumer of TypeScript, you&#8217;ll likely be unaffected.\nTypeScript won&#8217;t be shipping its API as ES modules yet, and will still provide a CommonJS-authored API.\nThat means existing build scripts will still work.\nIf you rely on TypeScript&#8217;s <code>typescriptServices.js<\/code> and <code>typescriptServices.d.ts<\/code> files, you&#8217;ll be able to rely on <code>typescript.js<\/code>\/<code>typescript.d.ts<\/code> instead.\nIf you&#8217;re importing <code>protocol.d.ts<\/code>, you can switch to <code>tsserverlibrary.d.ts<\/code> and leverage <code>ts.server.protocol<\/code>.<\/p>\n<p>Finally, as a contributor of TypeScript, your life will likely become a lot easier.\nBuild times will be a lot faster, incremental check times should be faster, and you&#8217;ll have a more familiar authoring format if you already write TypeScript code outside of our compiler.<\/p>\n<h2>Some Background<\/h2>\n<p>Now that might sound surprising &#8211; modules?\nLike, files with <code>import<\/code>s and <code>export<\/code>s?\nIsn&#8217;t almost all modern JavaScript and TypeScript using modules?<\/p>\n<p>Exactly!\nBut the current TypeScript codebase predates ECMAScript&#8217;s modules &#8211; our last rewrite started in 2014, and modules were standardized in 2015.\nWe didn&#8217;t know how (in)compatible they&#8217;d be with other module systems like CommonJS, and to be frank, there wasn&#8217;t a huge benefit for us at the time for authoring in modules.<\/p>\n<p>Instead, TypeScript leveraged <code>namespace<\/code>s &#8211; formerly called <em>internal modules<\/em>.<\/p>\n<p>Namespaces had a few useful features.\nFor example, their scopes could merge across files, meaning it was easy to break up a project across files and expose it cleanly as a single variable.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ parser.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ program.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ user.ts<\/span>\r\n\r\n<span>\/\/ Can easily access both functions from 'ts'.<\/span>\r\n<span>const<\/span><span> <\/span><span>sourceFile<\/span><span> = <\/span><span>ts<\/span><span>.<\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>const<\/span><span> <\/span><span>program<\/span><span> = <\/span><span>ts<\/span><span>.<\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<\/code><\/pre>\n<p>It was also easy for us to reference exports across files at a time when auto-import didn&#8217;t exist.\nCode in the same namespace could access each other&#8217;s exports without needing to write <code>import<\/code> statements.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ parser.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ program.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/\/ We can reference 'createSourceFile' without writing<\/span>\r\n<span>        <\/span><span>\/\/ 'ts.createSourceFile' or writing any sort of 'import'.<\/span>\r\n<span>        <\/span><span>let<\/span><span> <\/span><span>file<\/span><span> = <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>In retrospect, these features from namespaces made it difficult for other tools to support TypeScript;\nhowever, they were very useful for our codebase.<\/p>\n<p>Fast-forward several years, and we were starting to feel more of the downsides of namespaces.<\/p>\n<h2>Issues with Namespaces<\/h2>\n<p>TypeScript is written in TypeScript.\nThis occasionally surprises people, but it&#8217;s a common practice <a href=\"https:\/\/en.wikipedia.org\/wiki\/Bootstrapping_(compilers)\">for compilers to be written in the language they compile<\/a>.\nDoing this really helps us understand the experience we&#8217;re shipping to other JavaScript and TypeScript developers.\nThe jargon-y way to say this is: we <a href=\"https:\/\/en.wikipedia.org\/wiki\/Bootstrapping_(compilers)\">bootstrap<\/a> the TypeScript compiler so that we can <a href=\"https:\/\/en.wikipedia.org\/wiki\/Eating_your_own_dog_food\">dog-food<\/a> it.<\/p>\n<p>Most modern JavaScript and TypeScript code is authored using modules.\nBy using namespaces, we weren&#8217;t using TypeScript the way most of our users are.\n<em>So many<\/em> of our features are focused around using modules, but we weren&#8217;t using them ourselves.\nSo we had two issues here: we weren&#8217;t just missing out on these features &#8211; we were missing a ton of the experience in using those features.<\/p>\n<p>For example, TypeScript supports an <code>incremental<\/code> mode for builds.\nIt&#8217;s a great way to speed up consecutive builds, but it&#8217;s effectively useless in a codebase structured with namespaces.\nThe compiler can only effectively do incremental builds across <em>modules<\/em>, but our namespaces just sat within the global scope (which is usually where namespaces will reside).\nSo we were hurting our ability to iterate on TypeScript itself, along with properly testing out our <code>incremental<\/code> mode on our own codebase.<\/p>\n<p>This goes deeper than compiler features &#8211; experiences like error messages and editor scenarios are built around modules too.\nAuto-import completions and the &quot;Organize Imports&quot; command are two widely used editor features that TypeScript powers, and we weren&#8217;t relying on them at all.<\/p>\n<h2>Runtime Performance Issues with Namespaces<\/h2>\n<p>Some of the issues with namespaces are more subtle.\nUp until now, most of the issues with namespaces might have sounded like pure infrastructure issues &#8211; but namespaces also have a runtime performance impact.<\/p>\n<p>First, let&#8217;s take a look at our earlier example:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ parser.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ program.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>Those files will be rewritten to something like the following JavaScript code:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ parser.js<\/span>\r\n<span>var<\/span><span> <\/span><span>ts<\/span><span>;<\/span>\r\n<span>(<\/span><span>function<\/span><span> (<\/span><span>ts<\/span><span>) {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>    <\/span><span>ts<\/span><span>.<\/span><span>createSourceFile<\/span><span> = <\/span><span>createSourceFile<\/span><span>;<\/span>\r\n<span>})(<\/span><span>ts<\/span><span> || (<\/span><span>ts<\/span><span> = {}));<\/span>\r\n\r\n<span>\/\/ program.js<\/span>\r\n<span>(<\/span><span>function<\/span><span> (<\/span><span>ts<\/span><span>) {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>ts<\/span><span>.<\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>    }<\/span>\r\n<span>    <\/span><span>ts<\/span><span>.<\/span><span>createProgram<\/span><span> = <\/span><span>createProgram<\/span><span>;<\/span>\r\n<span>})(<\/span><span>ts<\/span><span> || (<\/span><span>ts<\/span><span> = {}));<\/span>\r\n<\/code><\/pre>\n<p>The first thing to notice is that each namespace is wrapped in an <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Glossary\/IIFE\">IIFE<\/a>.\nEach occurrence of a <code>ts<\/code> namespace has the same setup\/teardown that&#8217;s repeated over and over again &#8211; which <em>in theory<\/em> could be optimized away when producing a final output file.<\/p>\n<p>The second, more subtle, and more significant issue is that our reference to <code>createSourceFile<\/code> had to be rewritten to <code>ts.createSourceFile<\/code>.\nRecall that this was actually something we <em>liked<\/em> &#8211; it made it easy to reference exports across files.<\/p>\n<p>However, there is a runtime cost.\nUnfortunately, there are very few zero-cost abstractions in JavaScript, and invoking a method off of an object is more costly than directly invoking a function that&#8217;s in scope.\nSo running something like <code>ts.createSourceFile<\/code> is more costly than <code>createSourceFile<\/code>.<\/p>\n<p>The performance difference between these operations is usually negligible.\nOr at least, it&#8217;s negligible until you&#8217;re writing a compiler, where these operations occur millions of times over millions of nodes.\nWe realized this was a huge opportunity for us to improve a few years ago when <a href=\"https:\/\/github.com\/evanw\">Evan Wallace<\/a> pointed out this overhead <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/39247\">on our issue tracker<\/a>.<\/p>\n<p>But namespaces aren&#8217;t the only construct that can run into this issue &#8211; the way most bundlers emulate scopes hits the same problem.\nFor example, consider if the TypeScript compiler were structured using modules like the following:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ parser.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>\/*...*\/<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ program.ts<\/span>\r\n<span>import<\/span><span> { <\/span><span>createSourceFile<\/span><span> } <\/span><span>from<\/span><span> <\/span><span>&quot;.\/parser&quot;<\/span><span>;<\/span>\r\n\r\n<span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>A naive bundler might always create a function to establish scope for every module, and place exports on a single object.\nIt might look something like the following:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ Runtime helpers for bundle:<\/span>\r\n<span>function<\/span><span> <\/span><span>register<\/span><span>(<\/span><span>moduleName<\/span><span>, <\/span><span>module<\/span><span>) { <\/span><span>\/*...*\/<\/span><span> }<\/span>\r\n<span>function<\/span><span> <\/span><span>customRequire<\/span><span>(<\/span><span>moduleName<\/span><span>) { <\/span><span>\/*...*\/<\/span><span> }<\/span>\r\n\r\n<span>\/\/ Bundled code:<\/span>\r\n<span>register<\/span><span>(<\/span><span>&quot;parser&quot;<\/span><span>, <\/span><span>function<\/span><span> (<\/span><span>exports<\/span><span>, <\/span><span>require<\/span><span>) {<\/span>\r\n<span>    <\/span><span>exports<\/span><span>.<\/span><span>createSourceFile<\/span><span> = <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    };<\/span>\r\n<span>});<\/span>\r\n\r\n<span>register<\/span><span>(<\/span><span>&quot;program&quot;<\/span><span>, <\/span><span>function<\/span><span> (<\/span><span>exports<\/span><span>, <\/span><span>require<\/span><span>) {<\/span>\r\n<span>    <\/span><span>var<\/span><span> <\/span><span>parser<\/span><span> = <\/span><span>require<\/span><span>(<\/span><span>&quot;parser&quot;<\/span><span>);<\/span>\r\n\r\n<span>    <\/span><span>exports<\/span><span>.<\/span><span>createProgram<\/span><span> = <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>parser<\/span><span>.<\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>    };<\/span>\r\n<span>});<\/span>\r\n\r\n<span>var<\/span><span> <\/span><span>parser<\/span><span> = <\/span><span>customRequire<\/span><span>(<\/span><span>&quot;parser&quot;<\/span><span>);<\/span>\r\n<span>var<\/span><span> <\/span><span>program<\/span><span> = <\/span><span>customRequire<\/span><span>(<\/span><span>&quot;program&quot;<\/span><span>);<\/span>\r\n<span>module<\/span><span>.<\/span><span>exports<\/span><span> = {<\/span>\r\n<span>    <\/span><span>createSourceFile:<\/span><span> <\/span><span>parser<\/span><span>.<\/span><span>createSourceFile<\/span><span>,<\/span>\r\n<span>    <\/span><span>createProgram:<\/span><span> <\/span><span>program<\/span><span>.<\/span><span>createProgram<\/span><span>,<\/span>\r\n<span>};<\/span>\r\n<\/code><\/pre>\n<p>Each reference of <code>createSourceFile<\/code> now has to go through <code>parser.createSourceFile<\/code>, which would still have more runtime overhead compared to if <code>createSourceFile<\/code> was declared locally.\nThis is partially necessary to emulate the &quot;live binding&quot; behavior of ECMAScript modules &#8211; if someone modifies <code>createSourceFile<\/code> within <code>parser.ts<\/code>, it will be reflected in <code>program.ts<\/code> as well.\nIn fact, the JavaScript output here can get even worse, as re-exports often need to be defined in terms of <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/JavaScript\/Reference\/Functions\/get\">getters<\/a> &#8211; and the same is true for every intermediate re-export too!\nBut for our purposes, let&#8217;s just pretend bundlers always write properties and not getters.<\/p>\n<p>So if bundled modules can also run into these issues, why did we even mention the issues around boilerplate and indirection with namespaces?<\/p>\n<p>Well because the ecosystem around modules is rich, and bundlers have gotten surprisingly good at optimizing some of this indirection away!\nA growing number of bundling tools are able to not just aggregate multiple modules into one file, but they&#8217;re able to perform something called <em>scope hoisting<\/em>.\nScope hoisting attempts to move as much code as possible into the fewest possible shared scopes.\nSo a bundler which performs scope-hoisting might be able to rewrite the above as<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>\/*...*\/<\/span>\r\n<span>}<\/span>\r\n\r\n<span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>}<\/span>\r\n\r\n<span>module<\/span><span>.<\/span><span>exports<\/span><span> = {<\/span>\r\n<span>    <\/span><span>createSourceFile<\/span><span>,<\/span>\r\n<span>    <\/span><span>createProgram<\/span><span>,<\/span>\r\n<span>};<\/span>\r\n<\/code><\/pre>\n<p>Putting these declarations in the same scope is typically a win simply because it avoids adding boilerplate code to simulate scopes in a single file &#8211; lots of those scope setups and teardowns can be completely eliminated.\nBut because scope hoisting colocates declarations, it <em>also<\/em> makes it easier for engines to optimize our uses of different functions.<\/p>\n<p>So moving to modules was not just an opportunity to build empathy and iterate more easily &#8211; it was a chance for us to make things faster!<\/p>\n<h2>The Migration<\/h2>\n<p>Unfortunately there&#8217;s not a clear 1:1 translation for every codebase using namespaces to modules.<\/p>\n<p>We had some specific ideas of what we wanted our codebase to look like with modules.\nWe definitely wanted to avoid too much disruption to the codebase stylistically, and didn&#8217;t want to run into too many &quot;gotchas&quot; through auto-imports.\nAt the same time, our codebase had implicit cycles and that presented its own set of issues.<\/p>\n<p>To perform the migration, we worked on some tooling specific to our repository which we nicknamed the &quot;typeformer&quot;.\nWhile <a href=\"https:\/\/github.com\/weswigham\/typeformer\">early versions<\/a> used the TypeScript API directly, <a href=\"https:\/\/github.com\/jakebailey\/typeformer\/\">the most up-to-date version<\/a> used <a href=\"https:\/\/github.com\/dsherret\">David Sherret<\/a>&#8216;s fantastic <a href=\"https:\/\/ts-morph.com\/\">ts-morph<\/a> library.<\/p>\n<p>Part of the approach that made this migration tenable was to break each transformation into its own step and its own commit.\nThat made it easier to iterate on specific steps without having to worry about trivial but invasive differences like changes in indentation.\nEach time we saw something that was &quot;wrong&quot; in the transformation, we could iterate.<\/p>\n<p>A small (see: <em>very annoying<\/em>) snag on this transformation was how exports across modules are implicitly resolved.\nThis created some implicit cycles that were not always obvious, and which we didn&#8217;t really want to reason about immediately.<\/p>\n<p>But we were in luck &#8211; TypeScript&#8217;s API needed to be preserved through something called a &quot;barrel&quot; module &#8211; a single module that re-exports all the stuff from every other module.\nWe took advantage of that and applied an &quot;if it ain&#8217;t broke, don&#8217;t fix it (for now)&quot; approach when we generated imports.\nIn other words, in cases where we couldn&#8217;t create direct imports from each module, the typeformer simply generated imports from that barrel module.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ program.ts<\/span>\r\n<span>import<\/span><span> { <\/span><span>createSourceFile<\/span><span> } <\/span><span>from<\/span><span> <\/span><span>&quot;.\/_namespaces\/ts&quot;<\/span><span>; <\/span><span>\/\/ &lt;- not directly importing from '.\/parser'.<\/span>\r\n<\/code><\/pre>\n<p>We figured eventually, we could (and thanks to a proposed change from <a href=\"https:\/\/github.com\/a-tarasyuk\">Oleksandr Tarasiuk<\/a>, <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/pull\/51590\">we will<\/a>), switch to direct imports across files.<\/p>\n<h2>Picking a Bundler<\/h2>\n<p>There are some phenomenal new bundlers out there &#8211; so we thought about our requirements.\nWe wanted something that<\/p>\n<ul>\n<li>supported different module formats (e.g. CommonJS, ESM, hacky IIFEs that conditionally set globals&#8230;)<\/li>\n<li>provided good scope hoisting and tree shaking support<\/li>\n<li>was easy to configure<\/li>\n<li>was fast<\/li>\n<\/ul>\n<p>There are several options here that might have been equally good;\nbut in the end we went with esbuild and have been pretty happy with it!\nWe were struck with how fast it enabled our ability to iterate, and how quickly any issues we ran into were addressed.\nKudos to <a href=\"https:\/\/github.com\/evanw\">Evan Wallace<\/a> on not just helping uncover some nice performance wins, but also making such a stellar tool.<\/p>\n<h2>Bundling and Compiling<\/h2>\n<p>Adopting esbuild presented a sort of weird question though &#8211; should the bundler operate on TypeScript&#8217;s output, or directly on our TypeScript source files?\nIn other words, should TypeScript transform its <code>.ts<\/code> files and emit a series of <code>.js<\/code> files that esbuild will subsequently bundle?\nOr should esbuild compile <em>and<\/em> bundle our <code>.ts<\/code> files?<\/p>\n<p>The way <em>most<\/em> people use bundlers these days is the latter.\nIt avoids coordinating extra build steps, intermediate artifacts on disk for each step, and just tends to be faster.<\/p>\n<p>On top of that, esbuild supports a feature most other bundlers don&#8217;t &#8211; <code>const enum<\/code> inlining.\nThis inlining provides a crucial performance boost when traversing our data structures, and until recently the only major tool that supported it was the TypeScript compiler itself.\nSo esbuild made building directly from our input files truly possible with no runtime compromises.<\/p>\n<p>But TypeScript is also a compiler, and we need to test our own behavior!\nThe TypeScript compiler needs to be able to compile the TypeScript compiler and produce reasonable results, right?<\/p>\n<p>So while adding a bundler was helping us actually experience what we were shipping to our users, we were at risk of <em>losing<\/em> what it&#8217;s like to downlevel-compile ourselves and quickly see if everything still works.<\/p>\n<p>We ended up with a compromise.\nWhen running in CI, TypeScript will also be run as unbundled CommonJS emitted by <code>tsc<\/code>.\nThis ensures that TypeScript can still be bootstrapped, and can produce a valid working version of the compiler that passes our test suite.<\/p>\n<p>For local development, running tests still requires a full type-check from TypeScript by default, with compilation from esbuild.\nThis is partially necessary to run certain tests.\nFor example, we store a &quot;baseline&quot; or &quot;snapshot&quot; of TypeScript&#8217;s declaration files.\nWhenever our public API changes, we have to check the new <code>.d.ts<\/code> file against the baseline to see what&#8217;s changed; but producing declaration files requires running TypeScript anyway.<\/p>\n<p>But that&#8217;s just the default.\nWe can now easily run and debug tests without a full type-check from TypeScript if we really want.\nSo transforming JavaScript and type-checking have been decoupled for us, and can run independently if we need.<\/p>\n<h2>Preserving Our API and Bundling Our Declaration Files<\/h2>\n<p>As previously mentioned, one upside of using namespaces was that to create our output files, we could just concatenate our input files together.\nBut, this also applies to our output <em><code>.d.ts<\/code> files<\/em> as well.<\/p>\n<p>Given the earlier example:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ src\/compiler\/parser.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ src\/compiler\/program.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>        <\/span><span>createSourceFile<\/span><span>(); <\/span><span>\/*...*\/<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>Our original build system would produce a single output <code>.js<\/code> and <code>.d.ts<\/code> file.\nThe file <code>tsserverlibrary.d.ts<\/code> might look like this:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>): <\/span><span>\/* ...*\/<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>): <\/span><span>\/* ...*\/<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>When multiple <code>namespace<\/code>s exist in the same scope, they undergo something called <em>declaration merging<\/em>, where all their exports merge together.\nSo these <code>namespace<\/code>s formed a single final <code>ts<\/code> namespace and everything just worked.<\/p>\n<p>TypeScript&#8217;s API did have a few &quot;nested&quot; namespaces which we had to maintain during our migration.\nOne input file required to create <code>tsserverlibrary.js<\/code> looked like this:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ src\/server\/protocol.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span>.<\/span><span>server<\/span><span>.<\/span><span>protocol<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>type<\/span><span> <\/span><span>Request<\/span><span> = <\/span><span>\/*...*\/<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>Which, as an aside and refresher, is the same as writing this:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ src\/server\/protocol.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>namespace<\/span><span> <\/span><span>server<\/span><span> {<\/span>\r\n<span>        <\/span><span>export<\/span><span> <\/span><span>namespace<\/span><span> <\/span><span>protocol<\/span><span> {<\/span>\r\n<span>            <\/span><span>export<\/span><span> <\/span><span>type<\/span><span> <\/span><span>Request<\/span><span> = <\/span><span>\/*...*\/<\/span><span>;<\/span>\r\n<span>        }<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>and it would be tacked onto the bottom of <code>tsserverlibrary.d.ts<\/code>:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>): <\/span><span>\/* ...*\/<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>): <\/span><span>\/* ...*\/<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span>.<\/span><span>server<\/span><span>.<\/span><span>protocol<\/span><span> {<\/span>\r\n<span>    <\/span><span>type<\/span><span> <\/span><span>Request<\/span><span> = <\/span><span>\/*...*\/<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>and declaration merging would still work fine.<\/p>\n<p>In a post-namespaces world, we wanted to preserve the same API while using solely modules &#8211; and our declaration files had to be able to model this as well.<\/p>\n<p>To keep things working, each namespace in our public API was modeled by a single file which re-exported everything from individual smaller files.\nThese are often called &quot;barrel modules&quot; because they&#8230; uh&#8230; re-package everything in&#8230; a&#8230; barrel?<\/p>\n<p>We&#8217;re not sure.<\/p>\n<p>Anyway!\nThe way that we maintained the same public API was by using something like the following:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ COMPILER LAYER<\/span>\r\n\r\n<span>\/\/ src\/compiler\/parser.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>\/*...*\/<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ src\/compiler\/program.ts<\/span>\r\n<span>import<\/span><span> { <\/span><span>createSourceFile<\/span><span> } <\/span><span>from<\/span><span> <\/span><span>&quot;.\/_namespaces\/ts&quot;<\/span><span>;<\/span>\r\n\r\n<span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>);<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ src\/compiler\/_namespaces\/ts.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>*<\/span><span> <\/span><span>from<\/span><span> <\/span><span>&quot;.\/parser&quot;<\/span><span>;<\/span>\r\n<span>export<\/span><span> <\/span><span>*<\/span><span> <\/span><span>from<\/span><span> <\/span><span>&quot;.\/program&quot;<\/span><span>;<\/span>\r\n\r\n<span>\/\/ SERVER LAYER<\/span>\r\n\r\n<span>\/\/ src\/server\/protocol.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>type<\/span><span> <\/span><span>Request<\/span><span> = <\/span><span>\/*...*\/<\/span><span>;<\/span>\r\n\r\n<span>\/\/ src\/server\/_namespaces\/ts.server.protocol.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>*<\/span><span> <\/span><span>from<\/span><span> <\/span><span>&quot;..\/protocol&quot;<\/span><span>;<\/span>\r\n\r\n<span>\/\/ src\/server\/_namespaces\/ts.server.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>*<\/span><span> <\/span><span>as<\/span><span> <\/span><span>protocol<\/span><span> <\/span><span>from<\/span><span> <\/span><span>&quot;.\/protocol&quot;<\/span><span>;<\/span>\r\n\r\n<span>\/\/ src\/server\/_namespaces\/ts.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>*<\/span><span> <\/span><span>from<\/span><span> <\/span><span>&quot;..\/..\/compiler\/_namespaces\/ts&quot;<\/span><span>;<\/span>\r\n<span>export<\/span><span> <\/span><span>*<\/span><span> <\/span><span>as<\/span><span> <\/span><span>server<\/span><span> <\/span><span>from<\/span><span> <\/span><span>&quot;.\/ts.server&quot;<\/span><span>;<\/span>\r\n<\/code><\/pre>\n<p>Here, distinct namespaces in each of our projects were replaced with a barrel module in a folder called <code>_namespaces<\/code>.<\/p>\n<table>\n<thead>\n<tr>\n<th>Namespace<\/th>\n<th>Module Path within Project<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>namespace ts<\/code><\/td>\n<td><code>.\/_namespaces\/ts.ts<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>namespace ts.server<\/code><\/td>\n<td><code>.\/_namespaces\/ts.server.ts<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>namespace ts.server.protocol<\/code><\/td>\n<td><code>.\/_namespaces\/ts.server.protocol.ts<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>There is some &quot;needless&quot; indirection, but it provided a reasonable pattern for the modules transition.<\/p>\n<p>Now our <code>.d.ts<\/code> emit can of course handle this situation &#8211; each <code>.ts<\/code> file would produce a distinct output <code>.d.ts<\/code> file.\nThis is what most people writing TypeScript use;\nhowever, our situation has some unique features which make using it as-is challenging:<\/p>\n<ul>\n<li>Some consumers already rely on the fact that TypeScript&#8217;s API is represented in a single <code>d.ts<\/code> file.\nThese consumers include projects which expose the internals of TypeScript&#8217;s API (e.g. <code>ts-expose-internals<\/code>, <code>byots<\/code>), and projects\nwhich bundle\/wrap TypeScript (e.g. <code>ts-morph<\/code>).\nSo keeping things in a single file was desirable.<\/li>\n<li>We export many enums like <code>SyntaxKind<\/code> or <code>SymbolFlags<\/code> in our public API which are actually <code>const enum<\/code>s.\nExposing <code>const enum<\/code>s is generally A Bad Idea, as downstream TypeScript projects may accidentally assume these <code>enum<\/code>s&#8217; values never change and inline them.\nTo prevent that from happening, we need to post-process our declarations to remove the <code>const<\/code> modifier.\nThis would be challenging to keep track of over every single output file, so again, we probably want to keep things in a single file.<\/li>\n<li>Some downstream users <em>augment<\/em> TypeScript&#8217;s API, declaring that some of our internals exist;\nit&#8217;d be best to avoid breaking these cases even if they&#8217;re not <em>officially<\/em> supported, so whatever we ship needs to be similar enough to our old output to not cause any surprises.<\/li>\n<li>We track how our APIs change, and diff between the &quot;old&quot; and &quot;new&quot; APIs on every full test run.\nKeeping this limited to a single file is desirable.<\/li>\n<li>Given that each of our JavaScript library entry points are just single files, it really seemed like the most &quot;honest&quot; thing to do would be to ship single declaration files for each of those entry points.<\/li>\n<\/ul>\n<p>These all point toward one solution: declaration file bundling.<\/p>\n<p>Just like there are many options for bundling JavaScript, there are many options for bundling <code>.d.ts<\/code> files:\n<code>api-extractor<\/code>, <code>rollup-plugin-dts<\/code>, <code>tsup<\/code>, <code>dts-bundle-generator<\/code>, and so on.<\/p>\n<p>These all satisfy the end requirement of &quot;make a single file&quot;, however, the\nadditional requirement to produce a final output which declared our API in\nnamespaces similar to our old output meant that we couldn&#8217;t use any of them\nwithout a lot of modification.<\/p>\n<p>In the end, we opted to roll our own mini-<code>d.ts<\/code> bundler suited specifically for our needs.\nThis script clocks in at about 400 lines of code, naively walking each entry point&#8217;s exports recursively and emitting declarations as-is.\nGiven the previous example, this bundler outputs something like:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createSourceFile<\/span><span>(<\/span><span>\/*...*\/<\/span><span>): <\/span><span>\/* ...*\/<\/span><span>;<\/span>\r\n<span>    <\/span><span>function<\/span><span> <\/span><span>createProgram<\/span><span>(<\/span><span>\/*...*\/<\/span><span>): <\/span><span>\/* ...*\/<\/span><span>;<\/span>\r\n<span>    <\/span><span>namespace<\/span><span> <\/span><span>server<\/span><span> {<\/span>\r\n<span>        <\/span><span>namespace<\/span><span> <\/span><span>protocol<\/span><span> {<\/span>\r\n<span>            <\/span><span>type<\/span><span> <\/span><span>Request<\/span><span> = <\/span><span>\/*...*\/<\/span><span>;<\/span>\r\n<span>        }<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>This output is functionality equivalent to the old namespace-concatenation output, along with the same <code>const enum<\/code> to <code>enum<\/code> transformation and <code>@internal<\/code> removal that our previous output had.\nRemoving the repetition of <code>namespace ts { }<\/code> also made the declaration files slightly smaller (~200 KB).<\/p>\n<p>It&#8217;s important to note that this bundler is <em>not<\/em> intended for general use.\nIt naively walks imports and emits declarations <em>as-is<\/em>, and cannot\nhandle:<\/p>\n<ul>\n<li>\n<p><strong>Unexported Types<\/strong> &#8211; if an exported function references an unexported type, TypeScript&#8217;s <code>d.ts<\/code> emit will still declare the type locally.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>doSomething<\/span><span>(<\/span><span>obj<\/span><span>: <\/span><span>Options<\/span><span>): <\/span><span>void<\/span><span>;<\/span>\r\n\r\n<span>\/\/ Not exported, but used by 'doSomething'!<\/span>\r\n<span>interface<\/span><span> <\/span><span>Options<\/span><span> {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>This allows an API to talk about specific types, even if API consumers can&#8217;t actually refer to these types by name.<\/p>\n<p>Our bundler cannot emit unexported types, but can detect when it needs to be done, and issues an error indicating that the type must be exported.\nThis is a fine trade-off, since a complete API tends to be more usable.<\/p>\n<\/li>\n<li>\n<p><strong>Name Conflicts<\/strong> &#8211; two files may separately declare a type named <code>Info<\/code> &#8211; one which is exported, and the other which is purely local.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ foo.ts<\/span>\r\n<span>export<\/span><span> <\/span><span>interface<\/span><span> <\/span><span>Info<\/span><span> {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n<span>}<\/span>\r\n<span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>doFoo<\/span><span>(<\/span><span>info<\/span><span>: <\/span><span>Info<\/span><span>) {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ bar.ts<\/span>\r\n<span>interface<\/span><span> <\/span><span>Info<\/span><span> {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n<span>}<\/span>\r\n<span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>doBar<\/span><span>(<\/span><span>info<\/span><span>: <\/span><span>Info<\/span><span>) {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>This shouldn&#8217;t be a problem for a robust declaration bundler.\nThe unexported <code>Info<\/code> could be declared with a new name, and uses could be updated.<\/p>\n<p>But our declaration bundler isn&#8217;t robust &#8211; it doesn&#8217;t know how to do that.\nIts first attempt is to just drop the locally declared type, and keep the exported type.\nThis is very wrong, and it&#8217;s subtle because it usually doesn&#8217;t trigger any errors!<\/p>\n<p>We made the bundler a little smarter so that it can at least detect when this happens.\nIt now issues an error to fix the ambiguity, which can be done by renaming and exporting the missing type.\nThankfully, there were not many examples of this in the TypeScript API, as namespace merging already meant that declarations with the same name across files were merged.<\/p>\n<\/li>\n<li>\n<p><strong>Import Qualifiers<\/strong> &#8211; occasionally, TypeScript will infer a type that&#8217;s not imported locally.\nIn those cases, TypeScript will write that type as something like <code>import(&quot;.\/types&quot;).SomeType<\/code>.\nThese <code>import(...)<\/code> qualifiers can&#8217;t be left in the output since the paths they refer to don&#8217;t exist anymore.\nOur bundler detects these types, and requires that the code be fixed.\nTypically, this just means explicitly annotating the function with a type.\nBundlers like <code>api-extractor<\/code> can actually handle this case by rewriting the type reference to point at the correct type.<\/p>\n<\/li>\n<\/ul>\n<p>So while there were some limitations, for us these were all perfectly okay (and even desirable).<\/p>\n<h2>Flipping the Switch!<\/h2>\n<p>Eventually all these decisions and meticulous planning had to go somewhere!\nWhat was years in the making turned into <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/pull\/51387\">a hefty pull request with over <strong>282,000 lines<\/strong> changed<\/a>.\nPlus, the pull request had to be refreshed periodically given that we couldn&#8217;t freeze the TypeScript codebase for a long amount of time.\nIn a sense, we were trying to replace a bridge while our team was still driving on it.<\/p>\n<p>Luckily, the automation of our typeformer could re-construct each step of the migration with a commit, which also helped with review.\nOn top of that, our test suite and all of our external test infrastructure really gave us confidence to make the move.<\/p>\n<p>So finally, we asked our team to take a brief pause from making changes.\nWe hit that merge button, and just like that, <s>Jake convinced git he was the author of every line in the TypeScript codebase<\/s> TypeScript was using modules!<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/typescript\/wp-content\/uploads\/sites\/11\/2023\/03\/see-you-on-the-modules-side.png\" alt=\"Jake Bailey commenting &quot;Alright everyone, it's happening. See you on the other side.&quot; before merging his pull request\"><\/p>\n<h2>Wait, What Was That About Git?<\/h2>\n<p>Okay, we&#8217;re half joking about that git issue.\nWe do often use git blame to understand where a change came from, and unfortunately by default, git <em>does<\/em> think that almost every line came from our &quot;Convert the codebase to modules&quot; commit.<\/p>\n<p>Fortunately, git can be configured with <a href=\"https:\/\/git-scm.com\/docs\/git-blame#Documentation\/git-blame.txt-blameignoreRevsFile\"><code>blame.ignoreRevsFile<\/code><\/a> to ignore specific commits, and <a href=\"https:\/\/docs.github.com\/en\/repositories\/working-with-files\/using-files\/viewing-a-file#ignore-commits-in-the-blame-view\">GitHub ignores commits listed in a top-level <code>.git-blame-ignore-revs<\/code> files by default<\/a>.<\/p>\n<h2>Spring Cleaning<\/h2>\n<p>While we were making some of these changes, we looked for opportunities to simplify everything we were shipping.\nWe found that TypeScript had a few files that truthfully weren&#8217;t needed anymore.\n<code>lib\/typescriptServices.js<\/code> was the same as <code>lib\/typescript.js<\/code>, and all of <code>lib\/protocol.d.ts<\/code> was basically copied out of <code>lib\/tsserverlibrary.d.ts<\/code> from the <code>ts.server.protocol<\/code> namespace.<\/p>\n<p>In TypeScript 5.0, we chose to drop these files and recommend using these backward-compatible alternatives.\nIt was nice to shed a few megabytes while knowing we had good workarounds.<\/p>\n<h2>Spaces and Minifying?<\/h2>\n<p>One nice surprise we found from using esbuild was that on-disk size was reduced by more than we expected.\nIt turns out that a big reason for this is that esbuild uses 2 spaces for indentation in output instead of the 4 spaces that TypeScript uses.\nWhen gzipping, the difference is very small;\nbut on disk, we saved a considerable amount.<\/p>\n<p>This did prompt a question of whether we should start performing any minification on our outputs.\nAs tempting as it was, this would complicate our build process, make stack trace analysis harder, and force us to ship with source maps (or find a source map host, kind of like what a <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/debug\/symbol-servers-and-symbol-stores\">symbol server<\/a> does for debug information).<\/p>\n<p>We decided against minifying (for now).\nAnyone shipping parts of TypeScript on the web can already minify our outputs (which we do on the <a href=\"https:\/\/www.typescriptlang.org\/play\/\">TypeScript playground<\/a>), and gzipping already makes downloads from npm pretty fast.\nWhile minifying <em>felt<\/em> like &quot;low hanging fruit&quot; for an otherwise radical change to our build system, it was just creating more questions than answers.\nPlus, we have other better ideas for reducing our package size.<\/p>\n<h2>Performance Slowdowns?<\/h2>\n<p>When we dug a bit deeper, we noticed that while end-to-end compile times had reduced on all of our benchmarks, we had actually <em>slowed down<\/em> on parsing.\nSo what gives?<\/p>\n<p>We didn&#8217;t mention it much, but when we switched to modules, we also switched to a more modern emit target.\nWe switched from ECMAScript 5 to ECMAScript 2018.\nUsing more native syntax meant that we could shed a few bytes in our output, and would have an easier time debugging our code.\nBut it also meant that engines had to perform the <em>exact semantics<\/em> as mandated by these native constructs.<\/p>\n<p>You might be surprised to learn that <code>let<\/code> and <code>const<\/code> &#8211; two of the most commonly used features in modern JavaScript &#8211; have a little bit of overhead.<\/p>\n<p>That&#8217;s right!\n<code>let<\/code> and <code>const<\/code> variables can&#8217;t be referenced before their declarations have been run.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ error! 'x' is referenced in 'f'<\/span>\r\n<span>\/\/ before it's declared!<\/span>\r\n<span>f<\/span><span>();<\/span>\r\n\r\n<span>let<\/span><span> <\/span><span>x<\/span><span> = <\/span><span>10<\/span><span>;<\/span>\r\n\r\n<span>function<\/span><span> <\/span><span>f<\/span><span>() {<\/span>\r\n<span>    <\/span><span>console<\/span><span>.<\/span><span>log<\/span><span>(<\/span><span>x<\/span><span>);<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>And in order to enforce this, engines usually insert guards whenever <code>let<\/code> and <code>const<\/code> variables are captured by a function.\nEvery time a function references these variables, those guards have to occur at least once.<\/p>\n<p>When TypeScript targeted ECMAScript 5, these <code>let<\/code>s and <code>const<\/code>s were just transformed into <code>var<\/code>s.\nThat meant that if a <code>let<\/code> or <code>const<\/code>-declared variable was accessed before it was initialized, we wouldn&#8217;t get an error.\nInstead, its value would just be observed as <code>undefined<\/code>.\nThere had been instances where this difference meant that TypeScript&#8217;s downlevel-emit wasn&#8217;t behaving as per the spec.\nWhen we switched to a newer output target, we ended up fixing a few instances of use-before-declaration &#8211; but they were rare.<\/p>\n<p>When we finally flipped the switch to a more modern output target, we found that engines spent <em>a lot<\/em> of time performing these checks on <code>let<\/code> and <code>const<\/code>.\nAs an experiment, <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/pull\/52656\">we tried running Babel on our final bundle<\/a> to only transform <code>let<\/code> and <code>const<\/code> into <code>var<\/code>.\nWe found that often <em>10%-15% of our parse time<\/em> could be dropped from switching to <code>var<\/code> everywhere.\nThis translated to up to <em>5%<\/em> of our end-to-end compile time being just these <code>let<\/code>\/<code>const<\/code> checks!<\/p>\n<p>At the moment, esbuild doesn&#8217;t perform provide an option to transform <code>let<\/code> and <code>const<\/code> to <code>var<\/code>.\nWe could have used Babel here &#8211; but we really didn&#8217;t want to introduce another step into our build process.\n<a href=\"https:\/\/github.com\/syg\">Shu-yu Guo<\/a> has already been <a href=\"https:\/\/bugs.chromium.org\/p\/v8\/issues\/detail?id=13723\">investigating opportunities<\/a> to eliminate many of these runtime checks with some promising results &#8211; but some checks would still need to be run on every function, and we were looking for a win today.<\/p>\n<p>We instead found a compromise.\nWe realized that most major components of our compiler follow a pretty similar pattern where a top-level scope contains a good chunk of state that&#8217;s shared by other closures.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createScanner<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>text<\/span><span>;<\/span>\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>pos<\/span><span>;<\/span>\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>end<\/span><span>;<\/span>\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>token<\/span><span>;<\/span>\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>tokenFlags<\/span><span>;<\/span>\r\n\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>scanner<\/span><span> = {<\/span>\r\n<span>        <\/span><span>getToken<\/span><span>:<\/span><span> () <\/span><span>=&gt;<\/span><span> <\/span><span>token<\/span><span>,<\/span>\r\n<span>        <\/span><span>\/\/ ...<\/span>\r\n<span>    };<\/span>\r\n\r\n<span>    <\/span><span>return<\/span><span> <\/span><span>scanner<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>The biggest reason we really wanted to use <code>let<\/code> and <code>const<\/code> in the first place was because <code>var<\/code>s have the potential to leak scope out of blocks;\nbut at the top level scope of a function, there&#8217;s way fewer &quot;downsides&quot; to using <code>var<\/code>s.\nSo we asked ourselves how much performance we could win back by switching to <code>var<\/code> in just these contexts.<\/p>\n<p><a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/52924\">It turns out that we were able to get rid of most of these runtime checks by doing just that<\/a>!\nSo in a few select places in our compiler, we&#8217;ve switched to <code>var<\/code>s, where we turn off our &quot;no <code>var<\/code>&quot; ESLint rule just for those regions.\nThe <code>createScanner<\/code> function from above now looks like this:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>createScanner<\/span><span>(<\/span><span>\/*...*\/<\/span><span>) {<\/span>\r\n<span>    <\/span><span>\/\/ Why var? It avoids TDZ checks in the runtime which can be costly.<\/span>\r\n<span>    <\/span><span>\/\/ See: https:\/\/github.com\/microsoft\/TypeScript\/issues\/52924<\/span>\r\n<span>    <\/span><span>\/* eslint-disable no-var *\/<\/span>\r\n<span>    <\/span><span>var<\/span><span> <\/span><span>text<\/span><span>;<\/span>\r\n<span>    <\/span><span>var<\/span><span> <\/span><span>pos<\/span><span>;<\/span>\r\n<span>    <\/span><span>var<\/span><span> <\/span><span>end<\/span><span>;<\/span>\r\n<span>    <\/span><span>var<\/span><span> <\/span><span>token<\/span><span>;<\/span>\r\n<span>    <\/span><span>var<\/span><span> <\/span><span>tokenFlags<\/span><span>;<\/span>\r\n\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>scanner<\/span><span> = {<\/span>\r\n<span>        <\/span><span>getToken<\/span><span>:<\/span><span> () <\/span><span>=&gt;<\/span><span> <\/span><span>token<\/span><span>,<\/span>\r\n<span>        <\/span><span>\/\/ ...<\/span>\r\n<span>    };<\/span>\r\n<span>    <\/span><span>\/* eslint-enable no-var *\/<\/span>\r\n\r\n<span>    <\/span><span>return<\/span><span> <\/span><span>scanner<\/span><span>;<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>This isn&#8217;t something we&#8217;d recommend most projects do &#8211; at least not without profiling first.\nBut we&#8217;re happy we found a reasonable workaround here.<\/p>\n<h2>Where&#8217;s the ESM?<\/h2>\n<p>As we mentioned previously, while TypeScript is now written with modules, the actual JS files we ship have <em>not<\/em> changed format.\nOur libraries still act as CommonJS when executed in a CommonJS environment (<code>module.exports<\/code> is defined), or declare a top-level <code>var ts<\/code> otherwise (for <code>&lt;script&gt;<\/code>).<\/p>\n<p>There&#8217;s been a long-standing request for TypeScript to ship as ECMAScript modules (ESM) instead (<a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/32949\">#32949<\/a>).<\/p>\n<p>Shipping ECMAScript modules would have many benefits:<\/p>\n<ul>\n<li>Loading the ESM can be faster than un-bundled CJS if the runtime can load multiple files in parallel\n(even if they are executed in order).<\/li>\n<li>Native ESM doesn&#8217;t make use of export helpers, so an ESM output can be as fast as a bundled\/scope-hoisted output.\nCJS may need export helpers to simulate live bindings, and in codebases like ours where we have chains of re-exports, this can be slow.<\/li>\n<li>The package size would be smaller because we&#8217;re able to share code between our different entrypoints rather than making individual bundles.<\/li>\n<li>Those who bundle TypeScript could potentially tree shake parts they aren&#8217;t using.\nThis could even help the many users who <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/34617\">only need our parser<\/a>\n(though our codebase still needs more changes to make that work).<\/li>\n<\/ul>\n<p>That all sounds great!\nBut we aren&#8217;t doing that, so what&#8217;s up with that?<\/p>\n<p>The main reason comes down to the current ecosystem.\nWhile a lot of packages are adding ESM (or even going ESM only), an even greater portion are still using CommonJS.\nIt&#8217;s unlikely we can ship <em>only<\/em> ESM in the near future, so we have to keep shipping some CommonJS to not leave users behind.<\/p>\n<p>That being said, there is an interesting middle ground&#8230;<\/p>\n<h2>Shipping ESM Executables (And More?)<\/h2>\n<p>Previously, we mentioned that our <em>libraries<\/em> still act as CommonJS.\nBut, TypeScript isn&#8217;t just a library, it&#8217;s also a set of executables, including <code>tsc<\/code>, <code>tsserver<\/code>, as well as a few other smaller bundles for automatic type acquisition (ATA), file watching, and cancellation.<\/p>\n<p>The critical observation is that these executables <em>don&#8217;t need to be imported<\/em>;\nthey&#8217;re executables!\nBecause these don&#8217;t need to be imported by anyone (not even <a href=\"https:\/\/vscode.dev\">https:\/\/vscode.dev<\/a>, which uses <code>tsserverlibrary.js<\/code> and a custom host implementation), we are free to convert these executables to whatever module format we&#8217;d like, so long as the behavior doesn&#8217;t change for users invoking these executables.<\/p>\n<p>This means that, so long as we move our minimum Node version to v12.20, we could change <code>tsc<\/code>, <code>tsserver<\/code>, and so on, <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/51440\">into ESM<\/a>.<\/p>\n<p>One gotcha is that the path to our executables within our package are &quot;well known&quot;;\na surprising number of tools, <code>package.json<\/code> scripts, editor launch configurations, etc., use hard-coded paths like <code>.\/node_modules\/typescript\/bin\/tsc.js<\/code> or <code>.\/node_modules\/typescript\/lib\/tsc.js<\/code>.<\/p>\n<p>As our <code>package.json<\/code> doesn&#8217;t declare <a href=\"https:\/\/nodejs.org\/api\/packages.html#type\"><code>&quot;type&quot;: &quot;module&quot;<\/code><\/a>, Node assumes those files to be CommonJS, so emitting ESM isn&#8217;t enough.\nWe could try to use <code>&quot;type&quot;: &quot;module&quot;<\/code>, but that would add a whole slew of other challenges.<\/p>\n<p>Instead, we&#8217;ve been leaning towards just using a dynamic <code>import()<\/code> call within a CommonJS file to kick off an ESM file that will do the <em>actual<\/em> work.\nIn other words, we&#8217;d replace <code>tsc.js<\/code> with a wrapper like this:<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ https:\/\/en.wikipedia.org\/wiki\/Fundamental_theorem_of_software_engineering<\/span>\r\n<span>(() <\/span><span>=&gt;<\/span><span> <\/span><span>import<\/span><span>(<\/span><span>&quot;.\/esm\/tsc.mjs&quot;<\/span><span>))().<\/span><span>catch<\/span><span>((<\/span><span>e<\/span><span>) <\/span><span>=&gt;<\/span><span> {<\/span>\r\n<span>    <\/span><span>console<\/span><span>.<\/span><span>error<\/span><span>(<\/span><span>e<\/span><span>);<\/span>\r\n<span>    <\/span><span>process<\/span><span>.<\/span><span>exit<\/span><span>(<\/span><span>1<\/span><span>);<\/span>\r\n<span>});<\/span>\r\n<\/code><\/pre>\n<p>This would not be observable by anyone invoking the tool, and we&#8217;re now free to emit ESM.\nThen much of the code shared between <code>tsc.js<\/code>, <code>tsserver.js<\/code>, <code>typingsInstaller.js<\/code>, etc. could all be shared!\nThis would turn out to save another 7 MB in our package, which is great for a change which nobody can observe.<\/p>\n<p>What that ESM would actually look like and how it&#8217;s emitted is a different question.\nThe most compatible near-term option would be to use esbuild&#8217;s code splitting feature to emit ESM.<\/p>\n<p>Farther down the line, we could even fully convert the TypeScript codebase to a module format like <code>Node16<\/code>\/<code>NodeNext<\/code> or <code>ES2022<\/code>\/<code>ESNext<\/code>, and emit ESM directly!\nOr, if we still wanted to ship only a few files, we could expose our APIs as ESM files and turn them into a set of entry points for a bundler.\nEither way, there&#8217;s potential for making the TypeScript package on npm much leaner, but, it would be a <em>much<\/em> more difficult change.<\/p>\n<p>In any case, we&#8217;re absolutely thinking about this for the future; converting the codebase from namespaces to modules was the first big step in moving forward.<\/p>\n<h2>API Patching<\/h2>\n<p>As we mentioned, one of our goals was to maintain compatibility with TypeScript&#8217;s existing API;\nhowever, CommonJS modules <a href=\"https:\/\/www.hyrumslaw.com\/\">allowed people to use the TypeScript API in ways we did not anticipate<\/a>.<\/p>\n<p>In CommonJS, modules are plain objects, providing no default protection from others modifying your library&#8217;s internals!\nSo what we found over time was that lots of projects were <a href=\"https:\/\/en.wikipedia.org\/wiki\/Monkey_patch\">monkey-patching<\/a> our APIs!\nThis put us in a tough spot, because even if we wanted to support this patching, it would be (for all intents and purposes) infeasible.<\/p>\n<p>In many cases, we helped some projects move to more appropriate backwards-compatible APIs that we exposed.\nIn other cases, there are still some challenges helping our community move forward &#8211; but <a href=\"https:\/\/discord.com\/invite\/typescript\">we&#8217;re eager to chat and help project maintainers out<\/a>!<\/p>\n<h2>Accidentally Exported<\/h2>\n<p>On a related note, we aimed to keep some &quot;soft-compatibility&quot; around our existing APIs that were necessary due to our use of namespaces.<\/p>\n<p>With namespaces, internal functions <em>had<\/em> to be exported just so they could be used by different files.<\/p>\n<pre class=\"lang:default decode:true\" style=\"background-color: #f0f0f0;padding: 10px;border-radius: 10px;\"><code><span>\/\/ utilities.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>\/** <\/span><span>@internal<\/span><span> *\/<\/span>\r\n<span>    <\/span><span>export<\/span><span> <\/span><span>function<\/span><span> <\/span><span>doSomething<\/span><span>() {<\/span>\r\n<span>    }<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ parser.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>val<\/span><span> = <\/span><span>doSomething<\/span><span>();<\/span>\r\n<span>}<\/span>\r\n\r\n<span>\/\/ checker.ts<\/span>\r\n<span>namespace<\/span><span> <\/span><span>ts<\/span><span> {<\/span>\r\n<span>    <\/span><span>\/\/ ...<\/span>\r\n\r\n<span>    <\/span><span>let<\/span><span> <\/span><span>otherVal<\/span><span> = <\/span><span>doSomething<\/span><span>();<\/span>\r\n<span>}<\/span>\r\n<\/code><\/pre>\n<p>Here, <code>doSomething<\/code> had to be exported so that it could be accessed from other files.\nAs a special step in our build, we&#8217;d just erase them away from our <code>.d.ts<\/code> files if they were marked with a comment like <code>\/** @internal *\/<\/code>, but they&#8217;d still be reachable from the outside at run time.<\/p>\n<p>Bundling modules, in contrast, won&#8217;t leak the exports of every file&#8217;s exports.\nIf an entry point doesn&#8217;t re-export a function from another file, it will be copied in as a local.<\/p>\n<p>Technically with TypeScript 5.0, we could have not re-exported every <code>\/** @internal *\/<\/code>-marked function, and made them &quot;hard-privates&quot;.\nThis seemed unfriendly to projects experimenting with TypeScript&#8217;s APIs.\nWe also would need to start explicitly exporting everything in our public API.\nThat might be a best-practice, but it was more than we wanted to commit to for 5.0.<\/p>\n<p>We opted to keep our behavior the same in TypeScript 5.0.<\/p>\n<h2>How&#8217;s the Dog Food?<\/h2>\n<p>We claimed earlier that modules would help us empathize more with our users.\nHow true did that end up being?<\/p>\n<p>Well, first off, just consider all the packaging choices and build tool decisions we had to make!\nUnderstanding these issues has put us way closer to what other library authors currently experience, and it&#8217;s given us a lot of food for thought.<\/p>\n<p>But there were some obvious user experience problems we hit as soon as we switched to modules.\nThings like auto-imports and the &quot;Organize Imports&quot; command in our editors occasionally felt &quot;off&quot; and often conflicted with our linter preferences.\nWe also felt some pain around project references, where toggling flags between a &quot;development&quot; and a &quot;production&quot; build would have required a totally parallel set of <code>tsconfig.json<\/code> files.\nWe were surprised we hadn&#8217;t received more feedback about these issues from the outside, but we&#8217;re happy we caught them.\nAnd the best part is that many of these issues, like <a href=\"https:\/\/devblogs.microsoft.com\/typescript\/announcing-typescript-5-0-rc\/#case-insensitive-import-sorting-in-editors\">respecting case-insensitive import sorting<\/a> and <a href=\"https:\/\/devblogs.microsoft.com\/typescript\/announcing-typescript-5-0-rc\/#passing-emit-specific-flags-under-build\">passing emit-specific flags under <code>--build<\/code><\/a> are already implemented for TypeScript 5.0!<\/p>\n<p>What about project-level incrementality?\nIt&#8217;s not clear if we got the improvements that we were looking for.\nIncremental checking from <code>tsc<\/code> doesn&#8217;t happen in under a second or anything like that.\nWe think part of this might stem from cycles between files in each project.\nWe also think that because most of our work tends to be on large root files like our shared types, scanner, parser, and checker, it necessitates checking almost <em>every<\/em> other file in our project.\nThis is something we&#8217;d like to investigate in the future, and hopefully it translates to improvements for everyone.<\/p>\n<h2>The Results!<\/h2>\n<p>After all these steps, we achieved some great results!<\/p>\n<ul>\n<li>A 46% reduction in our uncompressed package size on npm<\/li>\n<li>A 10%-25% speed-up<\/li>\n<li>Lots of UX improvements<\/li>\n<li>A more modern codebase<\/li>\n<\/ul>\n<p>That performance improvement is a bit co-mingled with other performance work we&#8217;ve done in TypeScript 5.0 &#8211; but a surprising amount of it came from modules and scope-hoisting.<\/p>\n<p>We&#8217;re <em>ecstatic<\/em> about our faster, more modern codebase with its dramatically streamlined build.\nWe hope it makes TypeScript 5.0, and every future release, a joy for you to use.<\/p>\n<p>Happy Hacking!<\/p>\n<p>&#8211; Daniel Rosenwasser, Jake Bailey, and the TypeScript Team<\/p>\n","protected":false},"excerpt":{"rendered":"<p>One of the most impactful things we&#8217;ve worked on in TypeScript 5.0 isn&#8217;t a feature, a bug fix, or a data structure optimization. Instead, it&#8217;s an infrastructure change. In TypeScript 5.0, we restructured our entire codebase to use ECMAScript modules, and switched to a newer emit target. What to Know Now, before we dive in, [&hellip;]<\/p>\n","protected":false},"author":381,"featured_media":1797,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-3748","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-typescript"],"acf":[],"blog_post_summary":"<p>One of the most impactful things we&#8217;ve worked on in TypeScript 5.0 isn&#8217;t a feature, a bug fix, or a data structure optimization. Instead, it&#8217;s an infrastructure change. In TypeScript 5.0, we restructured our entire codebase to use ECMAScript modules, and switched to a newer emit target. What to Know Now, before we dive in, [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/posts\/3748","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/users\/381"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/comments?post=3748"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/posts\/3748\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/media\/1797"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/media?parent=3748"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/categories?post=3748"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/typescript\/wp-json\/wp\/v2\/tags?post=3748"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}