{"id":20637,"date":"2020-11-09T23:56:30","date_gmt":"2020-11-10T04:56:30","guid":{"rendered":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/"},"modified":"2023-08-04T09:17:14","modified_gmt":"2023-08-04T13:17:14","slug":"10-insights-adopting-typescript-at-scale","status":"publish","type":"post","link":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/","title":{"rendered":"10 Insights from Adopting TypeScript at Scale"},"content":{"rendered":"<div class='bbg-row bbg-bg--white  bbg-row--margin-top-none bbg-row--margin-bottom-none' data-anchor='row-6a07dc4bc9ccc'>\n  \n\t\n\t\n\t<div class=\"bbg-row--content\">\n\t\t\n\t\t\t<div class='bbg-column bbg-column--width-8 bbg-column--offset-2'>\n\t<div class='bb-wysiwyg'>\n    \n    <p><em>by Rob Palmer, JavaScript Infrastructure &amp; Tooling Lead at Bloomberg and Co-chair of TC39<\/em><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>A few years ago, Bloomberg Engineering decided to adopt <a href=\"https:\/\/www.typescriptlang.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">TypeScript<\/a> as a first-class supported language. This article shares some of the insights and lessons we learned during this journey.<\/p>\n<p>The headline is that we found TypeScript to be a strong net positive! Please keep that in mind when reading about some of the surprising corners we explored. As engineers, we are naturally attracted to seeing, solving and sharing problems, even when we&#8217;re having a good time?<\/p>\n\n<\/div>\n<figure class=\"image-figure image-figure--has-small-image\" data-animation=\"\">\n    <img loading=\"lazy\" decoding=\"async\" width=\"2048\" height=\"2048\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/ts_banana.png\" class=\"attachment-full size-full image-figure__image image-figure__image--primary\" alt=\"A stick of dynamite stylized to look like the TypeScript logo is lit and unpeeling from a yellow banana.\" \/><img loading=\"lazy\" decoding=\"async\" width=\"2048\" height=\"2048\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/ts_banana.png\" class=\"attachment-full size-full image-figure__image image-figure__image--small\" alt=\"A stick of dynamite stylized to look like the TypeScript logo is lit and unpeeling from a yellow banana.\" \/>\n    \n<\/figure>\n<div class='bb-wysiwyg'>\n    \n    <h2>Background<\/h2>\n<p>Bloomberg already had a colossal investment in JavaScript before TypeScript even existed &#8211; more than 50 million lines of JS code. Our main product is the Bloomberg Terminal, which contains more than 10,000 apps. The variety of apps is huge, ranging from the display of intensive real-time financial data and news to interactive trading solutions and many forms of messaging. Back in 2005, the company started migrating those apps from Fortran and C\/C++ to server-side JavaScript, with client-side JavaScript arriving around 2012. Today, we have more than 2,000 software engineers at the company writing JavaScript.<\/p>\n\n<\/div>\n<figure class=\"image-figure image-figure--has-small-image\" data-animation=\"\">\n    <img loading=\"lazy\" decoding=\"async\" width=\"2419\" height=\"1361\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/Wht_Dual_FullFront_001_Large_CMYK.jpg\" class=\"attachment-full size-full image-figure__image image-figure__image--primary\" alt=\"The Bloomberg Terminal\" \/><img loading=\"lazy\" decoding=\"async\" width=\"2419\" height=\"1361\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/Wht_Dual_FullFront_001_Large_CMYK.jpg\" class=\"attachment-full size-full image-figure__image image-figure__image--small\" alt=\"The Bloomberg Terminal\" \/>\n    <figcaption class='image-figure__caption'>The Bloomberg Terminal<\/figcaption>\n<\/figure>\n<div class='bb-wysiwyg'>\n    \n    <p>Transitioning this scale of codebase from plain JavaScript to TypeScript is a big deal. So we worked hard to ensure there was a thoughtful process that would keep us aligned with standards and preserve our existing capabilities to evolve and deploy our code quickly and safely.<\/p>\n<p>If you&#8217;ve ever been part of a technology migration in a large company, you may be used to heavy-handed project management being used to force progress from reluctant teams who would rather be working on new features. We found that adopting TypeScript was something altogether different. Engineers were self-starting conversions and championing the process! When we launched the beta version of our TypeScript platform support, more than 200 projects opted into TypeScript in the first year alone. Zero projects went back.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h2>What makes this usage of TypeScript special?<\/h2>\n<p>In addition to scale, something that makes this integration of TypeScript unique is that we have our own JavaScript runtime environment. This means that, in addition to well-known JavaScript host environments, such as browsers and Node, we also embed the V8 engine and Chromium directly to create our own JavaScript platform. The upside of this situation is that we can offer a simple developer experience in which TypeScript is supported directly by our platform and package ecosystem. Ryan Dahl&#8217;s Deno pursues similar ideas by putting TypeScript compilation into the runtime, whereas we keep it in tooling that is versioned independently of the runtime. An interesting consequence is that we get to explore what it&#8217;s like to exercise the TypeScript compiler in a standalone JS environment that spans both client and server and that does not use Node-specific conventions (e.g., there is no <code>node_modules<\/code> directory).<\/p>\n<p>Our platform supports an internal ecosystem of packages that uses a common tooling and publishing system. This allows us to encourage and enforce best practices, such as defaulting to TypeScript&#8217;s &#8220;strict mode,&#8221; as well as ensuring global invariants. For example, we guarantee that all published types are modular rather than global. It also means that engineers can focus on writing code rather than needing to figure out how to make TypeScript play nicely with a bundler or test framework. DevTools and error stacks use sourcemaps correctly. Tests can be written in TypeScript and code coverage is accurately expressed in terms of the original TypeScript code. It just works.<\/p>\n<p>We aim for regular TypeScript files to be the single source of truth for our APIs, as opposed to maintaining handwritten declaration files. This means we have a lot of code leaning heavily on the TypeScript compiler&#8217;s automatic generation of <code>.d.ts<\/code> declaration files from TypeScript source code. So when declaration-emit is not ideal, we notice it, as you will see.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h2>Principles<\/h2>\n<p>Let&#8217;s outline three key principles we&#8217;re striving for.<\/p>\n<p>\u2696\ufe0f <b>Scalability:<\/b> Development speed should be kept high as more packages adopt TypeScript. Time spent installing, compiling, and checking code should be minimized.<\/p>\n<p>\u262e\ufe0f <b>Ecosystem Coherence:<\/b>\u00a0Packages should work together. Upgrading dependencies should be pain-free.<\/p>\n<p><b>\ud83d\udcc4 Standards Alignment:<\/b> We want to stick with standards, such as ECMAScript, and be ready for where they might go next.<\/p>\n<p>The discoveries that surprised us usually came down to cases where we weren&#8217;t sure if we could preserve these principles.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h2>10 Learning Points<\/h2>\n<hr \/>\n<h3><strong>1. TypeScript can be JavaScript + Types<\/strong><\/h3>\n<p>Over the years, the TypeScript team has actively pursued the adoption of and alignment with standard ECMAScript syntax and runtime semantics. This leaves TypeScript to concentrate on providing a layer of type syntax and type-checking semantics on top of JavaScript. The responsibilities are clearly separated: <strong>TypeScript = JavaScript + Types!<\/strong><\/p>\n<p>This is a wonderful model. It means that the compiler output is human-readable JavaScript, just like the programmer wrote. This makes debugging production code easy even if you don&#8217;t have the original source code. It means you do not need to worry that choosing TypeScript might cut you off from future <a href=\"https:\/\/tc39.es\/\" target=\"_blank\" rel=\"noopener noreferrer\">ECMAScript<\/a> features. It leaves the door open to runtimes, and maybe even future JavaScript engines, that can ignore the type syntax and therefore &#8220;run&#8221; TypeScript natively. A simpler developer experience is in sight!<\/p>\n<p>Along the way, TypeScript was extended with a small number of features that don&#8217;t quite fit this model. <code>enum<\/code>, <code>namespace<\/code>, <em>parameter properties<\/em>, and <em>experimental decorators<\/em>\u00a0all have semantics that require them to be expanded into runtime code that, in all likelihood, will never be directly supported by JavaScript engines.<\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2754<\/b><\/p>\n<p>This is not a big deal. The <a href=\"https:\/\/github.com\/Microsoft\/TypeScript\/wiki\/TypeScript-Design-Goals#goals\" target=\"_blank\" rel=\"noopener noreferrer\">TypeScript Design Goals<\/a> articulate the need to avoid introducing more runtime features in the future. One member of the TypeScript team, <a href=\"https:\/\/twitter.com\/orta\" target=\"_blank\" rel=\"noopener noreferrer\">Orta<\/a>, created a meme-slide to emphasize this recognition.<\/p>\n\n<\/div>\n<figure class=\"image-figure image-figure--has-small-image\" data-animation=\"\">\n    <img loading=\"lazy\" decoding=\"async\" width=\"1282\" height=\"863\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TypeScript-Learnings-image2.png\" class=\"attachment-full size-full image-figure__image image-figure__image--primary\" alt=\"One member of the TypeScript team, Orta, created a meme-slide to emphasize the need to avoid introducing more runtime features in the future (as per the TypeScript Design Goals).\" \/><img loading=\"lazy\" decoding=\"async\" width=\"1282\" height=\"863\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TypeScript-Learnings-image2.png\" class=\"attachment-full size-full image-figure__image image-figure__image--small\" alt=\"One member of the TypeScript team, Orta, created a meme-slide to emphasize the need to avoid introducing more runtime features in the future (as per the TypeScript Design Goals).\" \/>\n    <figcaption class='image-figure__caption'>One member of the TypeScript team, Orta, created a meme-slide to emphasize the need to avoid introducing more runtime features in the future (as per the TypeScript Design Goals).<\/figcaption>\n<\/figure>\n<div class='bb-wysiwyg'>\n    \n    <p>Our toolchain addresses this set of undesirable features by preventing their use. This ensures that our growing TypeScript codebase is truly JS + Types.<\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>2. Keeping up with the Compiler is worthwhile<\/strong><\/h3>\n<p>TypeScript evolves <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/releases\" target=\"_blank\" rel=\"noopener noreferrer\">rapidly<\/a>. New versions of the language introduce new type-level features, add support for JavaScript features, improve performance and stability, as well as improve the type-checker to find more type errors. So there&#8217;s a lot of enticement to use new versions!<\/p>\n<p>Whilst TypeScript strives to preserve compatibility, these type-checking improvements represent breaking changes to the build process as new errors are identified in codebases that previously appeared error-free. Upgrading TypeScript therefore requires some intervention to get these benefits.<\/p>\n<p>There is another form of compatibility to consider, which is inter-project compatibility. As both JavaScript and TypeScript syntaxes evolve, declaration files need to contain new syntax.<\/p>\n<p>If a library upgrades TypeScript and starts producing modern declaration files with new syntax, application projects using that library will fail to compile if their version of TypeScript does not understand that syntax. An example of new declaration syntax is <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/pull\/33470\" target=\"_blank\" rel=\"noopener noreferrer\">the emit of getter\/setter accessors in TypeScript 3.7<\/a>. These are not understood by TypeScript 3.5 or earlier. This means that having an ecosystem of projects using different compiler versions is not ideal.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n<p>At Bloomberg, codebases are spread across various Git repositories that use common tooling. Despite not having a monorepo, we do have a centralized registry of TypeScript projects. This allowed us to create a Continuous Integration (CI) job to &#8220;build the world&#8221; and verify the build-time and run-time effects of compiler upgrades on every TypeScript project.<\/p>\n<p>This global checking is very powerful. We use this to assess Beta and RC releases of TypeScript to discover issues ahead of general release. Having a diverse corpus of real-world code means we also find edge cases. We use this system to guide fix-ups to projects ahead of the compiler upgrade, so that the upgrade itself is flawless. So far, this strategy has worked well and we have been able to keep the entire codebase on the latest version of TypeScript. This means we have not needed to employ mitigations such as <a href=\"https:\/\/github.com\/sandersn\/downlevel-dts\" target=\"_blank\" rel=\"noopener noreferrer\">downlevelling DTS files<\/a>.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>3. Consistent <code>tsconfig<\/code> settings are worthwhile<\/strong><\/h3>\n<p>Much of the flexibility provided by <code>tsconfig<\/code> is to allow you to adapt TypeScript to your runtime platform. In an environment where all projects are targeting the same evergreen runtime, it turns out to be a hazard for each project to configure this separately.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n<p>Therefore we made our toolchain responsible for generating <code>tsconfig<\/code> at build time with &#8220;ideal&#8221; settings. For example, <code><a href=\"https:\/\/www.typescriptlang.org\/tsconfig#strict\" target=\"_blank\" rel=\"noopener noreferrer\">\"strict\"<\/a><\/code> mode is enabled by default to increase type-safety. <a href=\"https:\/\/www.typescriptlang.org\/tsconfig#isolatedModules\" target=\"_blank\" rel=\"noopener noreferrer\"><code>\"isolatedModules\"<\/code><\/a> is enforced to ensure our code can be compiled quickly by simple transpilers that operate on a single file at a time.<\/p>\n<p>A further benefit of treating <code>tsconfig<\/code> as a generated file, rather than as a source file, is that it permits higher-level tooling to flexibly link together multi-project &#8220;workspaces&#8221; by taking responsibility for defining options such as <code><a href=\"https:\/\/www.typescriptlang.org\/tsconfig#references\" target=\"_blank\" rel=\"noopener noreferrer\">\"references\"<\/a><\/code> and <code><a href=\"https:\/\/www.typescriptlang.org\/tsconfig#paths\" target=\"_blank\" rel=\"noopener noreferrer\">\"paths\"<\/a><\/code>.<\/p>\n<p>There is some tension here, as a minority of projects wanted the ability to make customizations such as switching to looser modes to reduce the migration burden.<\/p>\n<p>Initially we tried to cater to these requests and gave access to a small number of options. Later we found that this resulted in inter-package conflicts, when declaration files built using one set of options were consumed by a package using different options. Here&#8217;s one example.<\/p>\n<p>It&#8217;s possible to create a conditional type that is directed by the value of <a href=\"https:\/\/www.typescriptlang.org\/tsconfig#strictNullChecks\"><code>\"strictNullChecks\"<\/code><\/a>.<b><\/b><\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #0000ff;\">type<\/span><span style=\"color: #000000;\"> A = unknown extends {} ? string : number;<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>If\u00a0<code>\"strictNullChecks\"<\/code> are enabled, then A is a <code>number<\/code>.\u00a0If <code>\"strictNullChecks\"<\/code> are disabled, then A is a <code>string<\/code>.\u00a0This code breaks if the package exporting this type is not using the same strictness settings as the package importing it.<\/p>\n<p>This is a simplified example of a real-life issue we faced.\u00a0As a result, we chose to deprecate the flexibility on strictness modes in favour of having consistent configs for all projects.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>4. How you specify the location of dependencies matters<\/strong><\/h3>\n<p>We needed the ability to explicitly declare the location of our dependencies to TypeScript. This is because our ES module system does not rely on the Node file-system convention of finding dependencies by walking up a series of directories named <code>node_modules<\/code>.<\/p>\n<p>We needed the ability to declare a mapping of bare-specifiers (e.g., <code>\"lodash\"<\/code>) to directory locations on disk (<code>\"c:dependencieslodash\"<\/code>). This is similar to the problem that <a href=\"https:\/\/github.com\/WICG\/import-maps\" target=\"_blank\" rel=\"noopener noreferrer\">import maps<\/a>\u00a0attempt to solve for the Web. At first, we tried using the <code><a href=\"https:\/\/www.typescriptlang.org\/tsconfig#paths\" target=\"_blank\" rel=\"noopener noreferrer\">\"paths\"<\/a><\/code> option in <code>tsconfig<\/code>.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ tsconfig.json<\/span>\r\n  <span style=\"color: #a31515;\">\"paths\"<\/span><span style=\"color: #000000;\">: {<\/span>\r\n    <span style=\"color: #a31515;\">\"lodash\"<\/span><span style=\"color: #000000;\">: [ <\/span><span style=\"color: #a31515;\">\"..\/..\/dependencies\/lodash\"<\/span><span style=\"color: #000000;\"> ]<\/span>\r\n<span style=\"color: #000000;\">  }<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>This worked great for nearly all use cases. However, we discovered this degraded the quality of generated declaration files. The TypeScript compiler necessarily injects synthetic import statements into declaration files to allow for composite types &#8211; where types can depend on types from other modules. When the synthetic imports reference types in dependencies, we found the <code>\"paths\"<\/code> approach injected a relative path (<code>import(\"..\/..\/dependencies\/lodash\")<\/code>) rather than preserving the bare-specifier (<code>import \"lodash\"<\/code>). For our system, the relative location of external package typings is an implementation detail that may change, so this was unacceptable.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n<p>The solution we found was to use <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/modules.html#ambient-modules\" target=\"_blank\" rel=\"noopener noreferrer\">Ambient Modules<\/a>:<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ ambient-modules.d.ts<\/span>\r\n<span style=\"color: #0000ff;\">declare<\/span> <span style=\"color: #0000ff;\">module<\/span> <span style=\"color: #a31515;\">\"lodash\"<\/span><span style=\"color: #000000;\"> {<\/span>\r\n  <span style=\"color: #0000ff;\">export<\/span> <span style=\"color: #0000ff;\">*<\/span> <span style=\"color: #0000ff;\">from<\/span> <span style=\"color: #a31515;\">\"..\/..\/dependencies\/lodash\"<\/span><span style=\"color: #000000;\">;<\/span>\r\n  <span style=\"color: #0000ff;\">export<\/span> <span style=\"color: #0000ff;\">default<\/span><span style=\"color: #000000;\"> from <\/span><span style=\"color: #a31515;\">\"..\/..\/dependencies\/lodash\"<\/span><span style=\"color: #000000;\">;<\/span>\r\n<span style=\"color: #000000;\">}<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>Ambient Modules are special. TypeScript&#8217;s declaration-emit preserves references to them rather than converting them to a relative path.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>5. De-duplicating types can be important<\/strong><\/h3>\n<p>App performance is critical, so we try to minimize the volume of JS that apps load at runtime. Our platform ensures that only one version of a package is used at runtime. This de-duplication of versions means that a given package cannot &#8220;freeze&#8221; or &#8220;pin&#8221; their dependencies. Consequently, this means packages must preserve compatibility over time.<\/p>\n<p>We wanted to provide the same &#8220;exactly-one&#8221; guarantee for types to ensure that, for a given compilation of a project, the type-check would only ever consider one single version of a package\u2019s dependencies. In addition to compile-time efficiency, the motivation was to ensure the type-checked world better reflects the runtime world. We specifically wanted to avoid staleness issues and &#8220;nominality hell,&#8221; in which two incompatible versions of nominal types are imported via a &#8220;diamond pattern&#8221;. This is a hazard that will likely grow as ecosystem adoption of nominal types increases.<\/p>\n<p style=\"text-align: center;\">\u2696\ufe0f <b>Scalability \u2754<\/b><\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n<p>We wrote a deterministic resolver that selects exactly one version of each dependency to type against based on the declared version constraints of the package being built.<\/p>\n<p style=\"text-align: center;\">\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n<p>This means the graph of type dependencies is dynamically assembled &#8211; it is not frozen. Whilst this unpinned dependency approach provides benefits and avoids some hazards, we later learned that it can introduce a different hazard due to subtle behavior in the TypeScript compiler. See <em><strong>9.<\/strong> <strong>Generated declarations can inline types from dependencies<\/strong><\/em>\u00a0to learn more.<\/p>\n<p>These trade-offs and choices are not specific to our platform. They apply equally to anyone publishing to DefinitelyTyped\/npm, and are determined by the aggregate effect of each package&#8217;s version constraints expressed in package.json <code>\"dependencies\"<\/code>.<\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>6. Implicit Type Dependencies should be avoided<\/strong><\/h3>\n<p>It&#8217;s easy to introduce global types in TypeScript. It&#8217;s even easier to depend on global types. If left unchecked, this means it is possible for hidden coupling to occur between distant packages. The TypeScript Handbook calls this out as being <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/declaration-files\/templates\/global-modifying-module-d-ts.html#global-modifying-modules\" target=\"_blank\" rel=\"noopener noreferrer\">&#8220;somewhat dangerous.&#8221;<\/a><\/p>\n<p style=\"text-align: center;\">\u2696\ufe0f <b>Scalability \u2754<\/b><\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ A declaration that injects global types<\/span>\r\n<span style=\"color: #0000ff;\">declare<\/span><span style=\"color: #000000;\"> global {<\/span>\r\n  <span style=\"color: #0000ff;\">interface<\/span><span style=\"color: #000000;\"> String {<\/span>\r\n<span style=\"color: #000000;\">    fancyFormat(opts?: StringFormatOptions): string;<\/span>\r\n<span style=\"color: #000000;\">  }<\/span>\r\n<span style=\"color: #000000;\">}<\/span>\r\n\r\n<span style=\"color: #008000;\">\/\/ Somewhere in a file far, far away...<\/span>\r\n<span style=\"color: #000000;\">String.fancyFormat();  <\/span><span style=\"color: #008000;\">\/\/ no error!<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>The solution to this is well-known: prefer explicit dependencies over global state. TypeScript has provided support for ECMAScript <code>import<\/code> and <code>export<\/code> statements for a long time, which achieve this goal.<\/p>\n<p>So the only remaining need was to prevent accidental creation of global types. Thankfully, it is possible to statically detect each of the cases where TypeScript permits the introduction of global types. So, we were able to update our toolchain to detect and error in the cases where these are used. This means we can safely rely on the fact that importing a package&#8217;s types is a side-effect-free operation.<\/p>\n<p style=\"text-align: center;\">\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>7. Declaration files have three export modes\u00a0<\/strong><\/h3>\n<p>Not all declaration files are equal. A declaration file operates in <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/38592#issue-619054264\" target=\"_blank\" rel=\"noopener noreferrer\">one of three modes<\/a>, depending on the content; specifically the usage of <code>import<\/code> and <code>export<\/code> keywords.<\/p>\n<ol>\n<li><b>global &#8212;<\/b> A declaration file with no usage of <code>import<\/code> or <code>export<\/code> will be considered to be <i>global<\/i>.\u00a0Top-level declarations are globally exported.<\/li>\n<li><b>module &#8212;<\/b> A declaration file with at least one <code>export<\/code> declaration will be considered to be a module. Only the <code>export<\/code> declarations are exported and no globals are defined.<\/li>\n<li><b>implicit exports &#8212;<\/b> A declaration file that has no <code>export<\/code> declarations, but does use <code>import<\/code> will trigger defined-yet-undocumented behaviour. This means that top-level declarations are treated as named <code>export<\/code> declarations and no globals are defined.<\/li>\n<\/ol>\n<p>We do not use the first mode. Our toolchain prevents global declaration files (see previous section: <em><strong>6.<\/strong> <strong>Implicit Type Dependencies should be avoided<\/strong><\/em>). This means all declaration files use ES module syntax.<\/p>\n<p style=\"text-align: center;\">\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n<p style=\"text-align: center;\"><b>\u262e\ufe0f Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2714\ufe0f<\/b><\/p>\n<p>Perhaps surprisingly, we found the slightly-spooky third-mode to be useful. By adding just a single-line self-import to the top of ambient declaration files, you can prevent them from polluting the global namespace: <code>import {} from \".\/&lt;my-own-name&gt;\";<\/code>. This one-liner made it trivial to convert third-party declarations, such as <code>lib.dom.d.ts<\/code>, to be modular and avoided the need to maintain a more complex fork.<\/p>\n<p>The TypeScript team <a href=\"https:\/\/twitter.com\/drosenwasser\/status\/1309196478326300674?s=20\" target=\"_blank\" rel=\"noopener noreferrer\">do not seem to love the third-mode<\/a>, so consider avoiding it where possible.<\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>8. Encapsulation of Packages can be violated<\/strong><\/h3>\n<p>As explained earlier (in <strong><em>5. De-duplicating types can be important<\/em><\/strong>), our use of unpinned dependencies means it is important for our packages to preserve not only runtime-compatibility, but also type-compatibility over time. That&#8217;s a challenge, so to make this preservation of compatibility practical, we have to really understand which types are exposed and must be constrained in this way. A first step is to explicitly differentiate public vs. private modules.<\/p>\n<p>Node recently gained this capability in the form of <a href=\"https:\/\/nodejs.org\/dist\/latest-v14.x\/docs\/api\/packages.html#packages_package_entry_points\" target=\"_blank\" rel=\"noopener noreferrer\">the package.json <code>\"exports\"<\/code> field<\/a>. This defines an encapsulation boundary by explicitly listing the files that are accessible from outside the package.<\/p>\n<p>Today, TypeScript is not aware of <i>package exports<\/i> and so does not have the concept of which files within a dependency are considered public or not. This becomes a problem during declaration generation, when TypeScript synthesizes import statements to transitive types in the emitted <code>.d.ts<\/code> file. It is not acceptable for our <code>.d.ts<\/code> files to reference private files in other packages. Here&#8217;s an example of it going wrong.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ index.ts<\/span>\r\n<span style=\"color: #0000ff;\">import<\/span><span style=\"color: #000000;\"> boxMaker <\/span><span style=\"color: #0000ff;\">from<\/span> <span style=\"color: #a31515;\">\"another-package\"<\/span>\r\n<span style=\"color: #0000ff;\">export<\/span> <span style=\"color: #0000ff;\">const<\/span><span style=\"color: #000000;\"> box = boxMaker();<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>The above source can lead to <code>tsc<\/code> emitting the following undesirable declaration.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ index.d.ts<\/span>\r\n<span style=\"color: #0000ff;\">export<\/span> <span style=\"color: #0000ff;\">const<\/span><span style=\"color: #000000;\"> box : import(<\/span><span style=\"color: #a31515;\">\"another-package\/private\"<\/span><span style=\"color: #000000;\">).Box<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>This is bad because <code>\"another-package\/private\"<\/code> is not part of that package&#8217;s compatibility promise, so might be moved or renamed without a SemVer major bump.\u00a0TypeScript today has no way of knowing it generated a fragile import.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n<p>We mitigate this problem using two steps:<\/p>\n<p>1. Our toolchain informs the TypeScript resolver of the intentionally-public bare-specifier paths that point to dependencies (e.g., <code>\"lodash\/public1\"<\/code>, <code>\"lodash\/public2\"<\/code>). We ensure TypeScript knows about the full set of legitimate dependency entry-points by silently adding type-only import statements to the bottom of the TypeScript files just before they flow into the compiler.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ user's source code<\/span>\r\n\r\n<span style=\"color: #008000;\">\/\/ injected by toolchain to assist declaration emit<\/span>\r\n<span style=\"color: #0000ff;\">import<\/span><span style=\"color: #000000;\"> type <\/span><span style=\"color: #0000ff;\">*<\/span> <span style=\"color: #0000ff;\">as<\/span><span style=\"color: #000000;\"> __fake_name_1 <\/span><span style=\"color: #0000ff;\">from<\/span> <span style=\"color: #a31515;\">\"lodash\/public1\"<\/span><span style=\"color: #000000;\">;<\/span>\r\n<span style=\"color: #0000ff;\">import<\/span><span style=\"color: #000000;\"> type <\/span><span style=\"color: #0000ff;\">*<\/span> <span style=\"color: #0000ff;\">as<\/span><span style=\"color: #000000;\"> __fake_name_2 <\/span><span style=\"color: #0000ff;\">from<\/span> <span style=\"color: #a31515;\">\"lodash\/public2\"<\/span><span style=\"color: #000000;\">;<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>When generating references to inferred transitive types, TypeScript&#8217;s declaration emit will prefer to use these existing namespace identifiers rather than synthesizing imports to private files.<\/p>\n<p>2. Our toolchain generates errors if TypeScript generates a path to a file in a dependency that we know is private. This is analogous to the existing TypeScript errors emitted when TypeScript realizes that it is generating a potentially hazardous path to a dependency.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <pre style=\"color: #000020; background: #f6f8ff;\">error TS2742<span style=\"color: #308080;\">:<\/span> The inferred type of <span style=\"color: #800000;\">'<\/span><span style=\"color: #1060b6;\">...<\/span><span style=\"color: #800000;\">'<\/span> cannot be named without a reference to <span style=\"color: #800000;\">'<\/span><span style=\"color: #1060b6;\">...<\/span><span style=\"color: #800000;\">'<\/span><span style=\"color: #308080;\">.<\/span>\r\nThis is likely not portable<span style=\"color: #308080;\">.<\/span> A type annotation is necessary<span style=\"color: #308080;\">.<\/span>\r\n<\/pre>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>This informs the user to work around the issue, by explicitly annotating their exports. Or, in some cases, they need to update the dependency to publicise internal types by directly exporting them from a public package entrypoint.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n<p>We look forward to TypeScript gaining first-class support for entrypoints so that workarounds like this are unnecessary.<\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>9. Generated declarations can inline types from dependencies<\/strong><\/h3>\n<p>Packages need to export <code>.d.ts<\/code> declarations so that users can consume them. We choose to use the TypeScript <code>declaration<\/code> option to generate <code>.d.ts<\/code> files from the original <code>.ts<\/code> files. Whilst it&#8217;s possible to hand-write and maintain <code>.d.ts<\/code> sibling files alongside regular code, this is less preferable because it is a hazard to keep them synchronized.<\/p>\n<p>TypeScript&#8217;s declaration emit works well most of the time. One issue we found was that <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/37151\" target=\"_blank\" rel=\"noopener noreferrer\">sometimes TypeScript will inline types from a dependency into the generated types (#37151)<\/a>. This means the type definition is relocated and potentially duplicated, rather than being referenced via an import statement. With structural typing, the compiler is not compelled to ensure types are referenced from one definition site &#8211; duplication of these types can be ok.<\/p>\n<p>We have seen <a href=\"https:\/\/twitter.com\/robpalmer2\/status\/1319188885197422594\" target=\"_blank\" rel=\"noopener noreferrer\">extreme cases<\/a> where this duplication has inflated declaration files from 7KB to 700KB.\u00a0That&#8217;s quite a lot of redundant code to download and parse.<\/p>\n<p style=\"text-align: center;\">\u00a0\u2696\ufe0f <b>Scalability \u2754<\/b><\/p>\n<p>Inlining of types within a package is not an ecosystem problem, because it is not externally visible. It becomes problematic when types are inlined across package boundaries, because it couples those two specific versions together. In our unpinned package system, packages can evolve independently. This means there is a risk of type incompatibility and, in particular, a risk of type staleness.<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2754<\/b><\/p>\n<p>Through experimentation, we discovered potential techniques to prevent inlining of type declarations, such as:<\/p>\n<ul>\n<li>prefer <code>interface<\/code> instead of <code>type<\/code> (interfaces are not inlined)\n<ul>\n<li>If an <code>interface<\/code> needed by a declaration is not exported, <a href=\"https:\/\/www.typescriptlang.org\/dev\/bug-workbench\/?ts=4.1.0-dev.20200917#code\/PTAEAEBMFMGMBsCGAnRAXAlgewHYCg8QIAzDeaHRAW2gC5QAHZDAN3WgDo0BnPDHNNGTFEsaKACSoAN55Q80Ino4ArlQBGQgNx4AvgWgAPBlmRpQxFTliZcFnKAAUASlD0pshaGTQ0K5A7SirQAjKC6OvqEYOCk5JQ09Awq6vAYsFy8GFQmZjLEOLoWyFhUoABEHMBMrOwcAFbc5QREQiXIoAAqAMoALAAMAEwAzPQAosamgpCgbMyIqeIA5CxLoAAWiNygpqAY2yrc-ADmoAnLEmvEJWVGggGI8KBUWJAq5BWImeWg6irmsEQOBwWHMmjO1GgkA4eCMuQBuG45hYoAAvPYXDo8EA\" target=\"_blank\" rel=\"noopener noreferrer\">tsc will refuse to inline the type and will generate a clear error<\/a> (e.g., <code>TS4023: Exported variable has or is using name from external module but cannot be named.<\/code>)<\/li>\n<li>If a <code>type<\/code> needed by a generated declaration is not exported, <a href=\"https:\/\/www.typescriptlang.org\/dev\/bug-workbench\/?ts=4.1.0-dev.20200917#code\/PTAEAEBMFMGMBsCGAnRAXAlgewHYCg8QIAzDeaHRAW2gC5QAHZDAN3WgDo0BnPNATwbRQAFVABeUAG88oOaET0cAVyoAjaMgDceAL4FoADwZZkaUMWU5YmXBZygAFAEpQ9MTPmhk0NMuQOUgq0AIygujr6hGDgpOSUNPQMymrwGLBcvBhUJmbSxDi6FshYVKAARBzATKzsHABW3OUERAByWKCaJcgAhHhGueawuNzmLBL2LjrRoADK0MIAItBqygDmoGiIapsd3AuMKWkZkJmgGDhpOBcbiA53uPxUWMrconhAA\" target=\"_blank\" rel=\"noopener noreferrer\">tsc will silently inline the type<\/a><\/li>\n<li>Nicholas Jamieson wrote <a href=\"https:\/\/ncjamieson.com\/prefer-interfaces\/\" target=\"_blank\" rel=\"noopener noreferrer\">an article on preferring interface over types<\/a>, including an ESlint rule<\/li>\n<\/ul>\n<\/li>\n<li>make types nominal (nominal types such as <code>enum<\/code> and <code>class<\/code> with private members are not inlined)<\/li>\n<li>add type annotations to exports\n<ul>\n<li><a href=\"https:\/\/www.typescriptlang.org\/dev\/bug-workbench\/?ts=4.1.0-dev.20200917#code\/PTAEAEBMFMGMBsCGAnRAXAlgewHYCg8QIAzDeaHRAW2gC5QAHZDAN3WgDo0BnPNATwbRQAFVABeUAG88oOaET0cAVyoAjaMgDceAL4FoADwZZkaUMWU5YmXBZygAFAEpQ9MTPmhk0NMuQOUgq0AIygujr6hGDgpOSUNPQMymrwGLBcvBhUJmbSxDi6FshYVKAARBzATKzs5QREAHJYoAJCCjg4WGjo2DgANKAA7sLc0MIYOGk4kwDmeEa55rC43OYsEvYuOtGgAMrjoAAi0GrKs62Iaq0tY8LJqekckJmgABaI3KBdoNlLvEA\" target=\"_blank\" rel=\"noopener noreferrer\">with no annotation we see inlining<\/a><\/li>\n<li><a href=\"https:\/\/www.typescriptlang.org\/dev\/bug-workbench\/?ts=4.1.0-dev.20200917#code\/PTAEAEBMFMGMBsCGAnRAXAlgewHYCg8QIAzDeaHRAW2gC5QAHZDAN3WgDo0BnPNATwbRQAFVABeUAG88oOaET0cAVyoAjaMgDceAL4FoADwZZkaUMWU5YmXBZygAFAEpQ9MTPmhk0NMuQOUgq0AIygujr6hGDgpOSUNPQMymrwGLBcvBhUJmbSxDi6FshYVKAARBzATKzs5QREIgAWwgJCCjg4WGjo2A5M0CwUPKAYOGk4YwDmogCEeEa55rC43OYsbqAASr7+OCKC0AA8bdBYxPYAfBL2LjrRoADK0MIAItBqyjM9aqBoWKBuC9GCk0hlIJlvNBiJoKLBpoxmGw0JwITw8EA\" target=\"_blank\" rel=\"noopener noreferrer\">with an explicit type annotation we force referencing behaviour<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p style=\"text-align: center;\">\u2696\ufe0f <b>Scalability \ud83e\udd14<\/b><\/p>\n<p style=\"text-align: center;\">\u00a0\u262e\ufe0f <b>Ecosystem Coherence \ud83e\udd14<\/b><\/p>\n<p>The inlining behaviour does not seem to be strictly specified. It is a side-effect of the way declaration files are constructed. So, the above methods may not work in future. We hope this is something that can be formalized in TypeScript. Until then we shall rely on user education to mitigate this risk.<\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>10. <\/strong><strong>Generated Declarations can contain non-essential dependencies<\/strong><\/h3>\n<p>Consumers of TypeScript declaration files typically only care about the public type API of a package. TypeScript declaration emit generates exactly one declaration file for every TypeScript file in a project. Some of this content can be irrelevant to users and can expose private implementation details. This behavior can be surprising to newcomers to TypeScript, who expect the typings to be a representation of the public API like the handwritten typings found on <a href=\"https:\/\/github.com\/DefinitelyTyped\/DefinitelyTyped\" target=\"_blank\" rel=\"noopener noreferrer\">Definitely Typed<\/a>.<\/p>\n<p>One example of this is <a href=\"https:\/\/www.typescriptlang.org\/dev\/bug-workbench\/?ts=4.1.0-dev.20200917#code\/PTAEAEBMFMGMBsCGAnRAXAlgewHYCg8QIAzDeaHRAW2gC5Q1oBnNAOjSb2gA8AHLZGlAYcjZMUSxooACrM0AEXSJQAbzyhNmgEb0WyEQHM8AXwKEw4UuUo16vAwDd00dpwxV+ghgE9e01Vl5JTQVE1BiZCwqUAAiVmBGFliCHi8hWFwWUBwsNABRPiwmaEgAIR8ABQBXbXgMWFB6ORYQlQBeNVBdOKS0AFpIZVjQM2AAKlB8x2hkHzQACyNQRG0sGYYlplB6nGkMbYxkZGhyZ1EGLFBqkuRtrGJNg9BeSQBrRENpceBUou8RGIJFJQABJC7qLQreg4apUbSzUx-dIRao4WCYXARHCgAAUAEommCIRotCc0NVkDjAoh6ABGUYAbiRBCIVjIFGodBetXqsDceA8KNUxBw4Ui0TiCQcGGcjBSXH+GSyQkcoE6ooJzLwQA\" target=\"_blank\" rel=\"noopener noreferrer\">generated declarations including typings for functions used only for internal testing<\/a>.<\/p>\n<p style=\"text-align: center;\">\u00a0\u2696\ufe0f <b>Scalability \u2754<\/b><b><\/b><\/p>\n\n<\/div>\n<figure class=\"image-figure image-figure--has-small-image\" data-animation=\"\">\n    <img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1078\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/autumn-1820615_1920.jpg\" class=\"attachment-full size-full image-figure__image image-figure__image--primary\" alt=\"A golden leaf-laden maple tree sheds leaves under a bold blue sky.\" \/><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1078\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/autumn-1820615_1920.jpg\" class=\"attachment-full size-full image-figure__image image-figure__image--small\" alt=\"A golden leaf-laden maple tree sheds leaves under a bold blue sky.\" \/>\n    \n<\/figure>\n<div class='bb-wysiwyg'>\n    \n    <p>Since our package system knows all the public package entrypoints, our tooling can crawl the graph of reachable types to identify all the types that do not need to be made public. This is Dead Type Elimination (DTE) or more precisely, Tree-Shaking. We wrote a tool to do this &#8211; it performs minimal work by <b>only<\/b> eliminating code from declaration files. It does not rewrite or relocate code &#8211; it is not a bundler. This means the published declarations are an unchanged subset of the TypeScript-generated declarations.<\/p>\n<p>Reducing the volume of published types has several advantages:<\/p>\n<ul>\n<li>it decreases the coupling to other packages (some packages do not re-export types from their dependencies)<\/li>\n<li>it aids encapsulation by preventing fully-private types from leaking<\/li>\n<li>it decreases the count and size of the published declaration files that need to be downloaded and unpacked by users<\/li>\n<li>it decreases the volume of code the TypeScript compiler has to parse when type-checking<\/li>\n<\/ul>\n<p>The &#8220;shaking&#8221; can have a dramatic effect. We&#8217;ve seen several packages where &gt;90% of the files and &gt;90% of the lines of types can be dropped.<\/p>\n<p style=\"text-align: center;\">\u00a0\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h2>Some options have sharp edges<\/h2>\n<p>We found a few surprises in the semantics of some of the <code>tsconfig<\/code> options.<\/p>\n<h3><strong>Mandated <code>baseUrl<\/code> in <\/strong><code><strong>tsconfig<\/strong><\/code><\/h3>\n<p>In TypeScript 4.0. if you want to use Project References or <code>\"paths\"<\/code>, you are required to also specify a <code>baseUrl<\/code>. This has the side effect of causing all bare-specifier imports to resolve relative to your project&#8217;s root directory.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <div class=\"code-container\">\n<pre><span style=\"color: #008000;\">\/\/ package-a\/main.ts<\/span>\r\n<span style=\"color: #0000ff;\">import<\/span> <span style=\"color: #a31515;\">\"sibling\"<\/span>   <span style=\"color: #008000;\">\/\/ Will auto-complete and type-check if `package-a\/sibling.js` exists<\/span><\/pre>\n<\/div>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <p>The hazard is that if you want to introduce any form of <code>\"paths\"<\/code>, it carries the additional implication that <code>import \"sibling\"<\/code> will be undesirably interpreted by TypeScript as an import of <code>&lt;project-root&gt;\/sibling.js<\/code> from inside your source directory.<\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2754<\/b><\/p>\n<p>To work around this, we used an unspeakable <code>baseUrl<\/code>. Using a null character prevents the undesirable bare auto-completions. We don&#8217;t recommend you try this at home.<\/p>\n<p>We reported <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/31869\" target=\"_blank\" rel=\"noopener noreferrer\">this on the TypeScript issue tracker<\/a> and were thrilled to see that <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/pull\/40101\" target=\"_blank\" rel=\"noopener noreferrer\">Andrew has solved this for TypeScript 4.1<\/a>, which will enable us to say goodbye to the null character!<\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2714\ufe0f<\/b><\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3><strong>JSON modules imply synthetic default imports<\/strong><\/h3>\n<p>If you want to use <code>\"resolveJsonModules\"<\/code>, you are required to also enable <code>\"useSyntheticDefaultImports\"<\/code> in order for TypeScript to see the JSON module as a default import. Using default imports is likely to become the way that Node and the Web handle JSON modules in future.<\/p>\n<p>Enabling <code>\"useSyntheticDefaultImports\"<\/code> has the unfortunate consequence of artificially allowing default imports from regular ES modules that do not have a default export! This is a hazard that you will only find out about when you come to run the code and it quickly falls over.<\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2754<\/b><\/p>\n<p>Ideally, there should be a way to import JSON modules that does not involve globally enabling synthetic defaults.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h2>The Great Parts<\/h2>\n<p>It&#8217;s worth calling out some of the particularly good things we&#8217;ve seen from TypeScript along the way from a tooling perspective.<\/p>\n<p><b>Incremental builds<\/b> have been essential. <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/release-notes\/typescript-3-6.html#apis-to-support---build-and---incremental\" target=\"_blank\" rel=\"noopener noreferrer\">API support for incremental builds<\/a> was a huge boost to us in TypeScript 3.6, allowing custom toolchains to have fast rebuilds.\u00a0After we reported <a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/37867\" target=\"_blank\" rel=\"noopener noreferrer\">a performance issue<\/a> when combining <code>incremental<\/code> with <code>noEmitOnError<\/code>, <a href=\"https:\/\/twitter.com\/sheetalkamat\" target=\"_blank\" rel=\"noopener noreferrer\">Sheetal<\/a> made them <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/release-notes\/typescript-4-0.html#speed-improvements-in-build-mode-with---noemitonerror\" target=\"_blank\" rel=\"noopener noreferrer\">even faster in TypeScript 4.0.<\/a><\/p>\n<p style=\"text-align: center;\">\u00a0\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n<p><a href=\"https:\/\/www.typescriptlang.org\/tsconfig#isolatedModules\" target=\"_blank\" rel=\"noopener noreferrer\"><b><code>\"isolatedModules\"<\/code><\/b><\/a> was vital to ensure we can perform fast standalone (one in, one out) transpilation. The TypeScript team fixed a bunch of issues to improve this option including:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/29490\" target=\"_blank\" rel=\"noopener noreferrer\">allowing <code>emitDeclaration<\/code> with <code>isolatedModules<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/31012\" target=\"_blank\" rel=\"noopener noreferrer\">allowing <code>noEmitOnError<\/code> with <code>isolatedModules<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/microsoft\/TypeScript\/issues\/32662\" target=\"_blank\" rel=\"noopener noreferrer\">clarifying that types <i>must<\/i> be exported explicitly with <code>isolatedModules<\/code><\/a><\/li>\n<\/ul>\n<p style=\"text-align: center;\">\u00a0\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n<p><a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/release-notes\/typescript-3-0.html#project-references\" target=\"_blank\" rel=\"noopener noreferrer\"><b>Project References<\/b><\/a> are the key to providing a seamless IDE experience. We leverage them greatly to make multi-package workspace-based development as slick as single-project development. Thanks to <a href=\"https:\/\/twitter.com\/sheetalkamat\" target=\"_blank\" rel=\"noopener noreferrer\">Sheetal<\/a>, they are now even better and support file-less <a href=\"https:\/\/devblogs.microsoft.com\/typescript\/announcing-typescript-3-9\/#solution-style-tsconfig\" target=\"_blank\" rel=\"noopener noreferrer\">&#8220;solution-style&#8221; tsconfigs.<\/a><\/p>\n<p style=\"text-align: center;\">\u00a0\u2696\ufe0f <b>Scalability \u2714\ufe0f<\/b><\/p>\n<p><a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/release-notes\/typescript-3-8.html#type-only-imports-and-export\" target=\"_blank\" rel=\"noopener noreferrer\"><b>Type-only imports<\/b><\/a> have been super useful. We use them everywhere to safely distinguish runtime imports from compile-time imports. They are essential for certain patterns using <code>\"isolatedModules\"<\/code> and allowed us to use <a href=\"https:\/\/www.typescriptlang.org\/tsconfig#importsNotUsedAsValues\" target=\"_blank\" rel=\"noopener noreferrer\"><code>\"importsNotUsedAsValues\": \"error\"<\/code><\/a> for maximum safety.\u00a0Thanks to <a href=\"https:\/\/twitter.com\/atcb\" target=\"_blank\" rel=\"noopener noreferrer\">Andrew<\/a> for delivering this!<\/p>\n<p style=\"text-align: center;\">\u262e\ufe0f <b>Ecosystem Coherence \u2714\ufe0f<\/b><\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2714\ufe0f<\/b><\/p>\n<p><a href=\"https:\/\/www.typescriptlang.org\/tsconfig#useDefineForClassFields\" target=\"_blank\" rel=\"noopener noreferrer\"><b><code>\"useDefineForClassFields\"<\/code><\/b><\/a> was important for ensuring our emitted ESNext code does not get rewritten, preserving the JS + Types nature of the language.\u00a0It means we can natively use Class Fields.\u00a0Thanks to <a href=\"https:\/\/twitter.com\/sanders_n\" target=\"_blank\" rel=\"noopener noreferrer\">Nathan<\/a> for providing this and making the migration process as smooth as possible.<\/p>\n<p style=\"text-align: center;\"><b>\ud83d\udcc4 Standards Alignment \u2714\ufe0f<\/b><\/p>\n<p>Feature delivery in TypeScript has been very fortuitous. Each time we realized we needed a feature, we frequently discovered it was already being delivered in the next version.<\/p>\n\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h2>Conclusion<\/h2>\n<p>The end result is that TypeScript is now a first-class language for our application platform. Integrating TypeScript with yet-another-runtime shows that the language and compiler seems to be just as flexible as JavaScript &#8211; they can both be used pretty much anywhere.<\/p>\n\n<\/div>\n<figure class=\"image-figure image-figure--has-small-image\" data-animation=\"\">\n    <img loading=\"lazy\" decoding=\"async\" width=\"2048\" height=\"2048\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/ts_mushroom.png\" class=\"attachment-full size-full image-figure__image image-figure__image--primary\" alt=\"A JavaScript box exploding to reveal TypeScript inside.\" \/><img loading=\"lazy\" decoding=\"async\" width=\"2048\" height=\"2048\" src=\"https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&amp;type=webp&amp;url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/ts_mushroom.png\" class=\"attachment-full size-full image-figure__image image-figure__image--small\" alt=\"A JavaScript box exploding to reveal TypeScript inside.\" \/>\n    \n<\/figure>\n<div class='bb-wysiwyg'>\n    \n    <p>Whilst we had to learn a lot along the way, nothing was insurmountable. When we needed support, we were pleasantly surprised at the responses from both the community and the TypeScript team themselves. A clear benefit of using shared open source technology is that when you have a problem, more often than not you find you are not alone. And when you find answers, you get the joy of sharing them.<\/p>\n\n<\/div>\n<div class=\"bb-separator\" data-color=\"\">\n\t<hr class=\"bb-separator__rule\">\n<\/div>\n<div class='bb-wysiwyg'>\n    \n    <h3>Acknowledgements<\/h3>\n<p><em>Many thanks for the reviews from Thomas Chetwin, Robin Ricard, Kubilay Kahveci, Scott Whittaker, Daniel Rosenwasser, Nathan Shively-Sanders, Titian Dragomir, and Maxwell Heiber. And thanks to Orta for the Twoslash code-formatting.<\/em><\/p>\n<p><em>The Banana &amp; Box graphics were created by Max Duluc.<\/em><\/p>\n\n<\/div>\n\n<\/div>\n\n\n\t\t\n\t<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Insights &amp; lessons learned during Bloomberg Engineering&#8217;s journey to adopt TypeScript as a first-class supported language<\/p>\n","protected":false},"author":184,"featured_media":18805,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1466],"tags":[1471,1490,1464,1598],"class_list":["post-20637","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech-at-bloomberg","tag-general","tag-javascript","tag-open-source","tag-typescript"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v19.11 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>10 Insights from Adopting TypeScript at Scale | Bloomberg LP<\/title>\n<meta name=\"description\" content=\"Rob Palmer shares some of the insights &amp; lessons learned during Bloomberg Engineering&#039;s journey to adopt TypeScript as a first-class supported language.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"10 Insights from Adopting TypeScript at Scale | Bloomberg LP\" \/>\n<meta property=\"og:description\" content=\"Rob Palmer shares some of the insights &amp; lessons learned during Bloomberg Engineering&#039;s journey to adopt TypeScript as a first-class supported language.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/\" \/>\n<meta property=\"og:site_name\" content=\"Bloomberg L.P.\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/bloomberglp\/\" \/>\n<meta property=\"article:published_time\" content=\"2020-11-10T04:56:30+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2023-08-04T13:17:14+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TechatBloomberg-11.20.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"617\" \/>\n\t<meta property=\"og:image:height\" content=\"403\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"chaas30\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TechatBloomberg-11.20.jpg\" \/>\n<meta name=\"twitter:creator\" content=\"@bloomberg\" \/>\n<meta name=\"twitter:site\" content=\"@bloomberg\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"chaas30\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"24 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/\",\"url\":\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/\",\"name\":\"10 Insights from Adopting TypeScript at Scale | Bloomberg LP\",\"isPartOf\":{\"@id\":\"https:\/\/www.bloomberg.com\/company\/#website\"},\"datePublished\":\"2020-11-10T04:56:30+00:00\",\"dateModified\":\"2023-08-04T13:17:14+00:00\",\"author\":{\"@id\":\"https:\/\/www.bloomberg.com\/company\/#\/schema\/person\/4d4a18aae79d6fcc1ea98181a906905e\"},\"description\":\"Rob Palmer shares some of the insights & lessons learned during Bloomberg Engineering's journey to adopt TypeScript as a first-class supported language.\",\"breadcrumb\":{\"@id\":\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":\"1\",\"name\":\"Home\",\"item\":\"https:\/\/www.bloomberg.com\/company\/\"},{\"@type\":\"ListItem\",\"position\":\"2\",\"name\":\"10 Insights from Adopting TypeScript at Scale\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.bloomberg.com\/company\/#website\",\"url\":\"https:\/\/www.bloomberg.com\/company\/\",\"name\":\"Bloomberg L.P.\",\"description\":\"Bloomberg L.P. is the leader in global business and financial information, enabling customers to make smarter, faster, more informed business decisions.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.bloomberg.com\/company\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.bloomberg.com\/company\/#\/schema\/person\/4d4a18aae79d6fcc1ea98181a906905e\",\"name\":\"Bloomberg L.P.\",\"url\":\"https:\/\/www.bloomberg.com\/company\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"10 Insights from Adopting TypeScript at Scale | Bloomberg LP","description":"Rob Palmer shares some of the insights & lessons learned during Bloomberg Engineering's journey to adopt TypeScript as a first-class supported language.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/","og_locale":"en_US","og_type":"article","og_title":"10 Insights from Adopting TypeScript at Scale | Bloomberg LP","og_description":"Rob Palmer shares some of the insights & lessons learned during Bloomberg Engineering's journey to adopt TypeScript as a first-class supported language.","og_url":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/","og_site_name":"Bloomberg L.P.","article_publisher":"https:\/\/www.facebook.com\/bloomberglp\/","article_published_time":"2020-11-10T04:56:30+00:00","article_modified_time":"2023-08-04T13:17:14+00:00","og_image":[{"width":617,"height":403,"url":"https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TechatBloomberg-11.20.jpg","type":"image\/jpeg"}],"author":"chaas30","twitter_card":"summary_large_image","twitter_image":"https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TechatBloomberg-11.20.jpg","twitter_creator":"@bloomberg","twitter_site":"@bloomberg","twitter_misc":{"Written by":"chaas30","Est. reading time":"24 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/","url":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/","name":"10 Insights from Adopting TypeScript at Scale | Bloomberg LP","isPartOf":{"@id":"https:\/\/www.bloomberg.com\/company\/#website"},"datePublished":"2020-11-10T04:56:30+00:00","dateModified":"2023-08-04T13:17:14+00:00","author":{"@id":"https:\/\/www.bloomberg.com\/company\/#\/schema\/person\/4d4a18aae79d6fcc1ea98181a906905e"},"description":"Rob Palmer shares some of the insights & lessons learned during Bloomberg Engineering's journey to adopt TypeScript as a first-class supported language.","breadcrumb":{"@id":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.bloomberg.com\/company\/stories\/10-insights-adopting-typescript-at-scale\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":"1","name":"Home","item":"https:\/\/www.bloomberg.com\/company\/"},{"@type":"ListItem","position":"2","name":"10 Insights from Adopting TypeScript at Scale"}]},{"@type":"WebSite","@id":"https:\/\/www.bloomberg.com\/company\/#website","url":"https:\/\/www.bloomberg.com\/company\/","name":"Bloomberg L.P.","description":"Bloomberg L.P. is the leader in global business and financial information, enabling customers to make smarter, faster, more informed business decisions.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.bloomberg.com\/company\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.bloomberg.com\/company\/#\/schema\/person\/4d4a18aae79d6fcc1ea98181a906905e","name":"Bloomberg L.P.","url":"https:\/\/www.bloomberg.com\/company"}]}},"featured_image_rendered":"<img srcset='' src='https:\/\/assets.bbhub.io\/image\/v1\/resize?width=auto&type=webp&url=https:\/\/assets.bbhub.io\/company\/sites\/51\/2020\/11\/TechatBloomberg-11.20.jpg' alt='Our codebase + TypeScript. The fuse is lit...' \/>","category_info":{"name":"Tech At Bloomberg","blog_landing_name":"Tech At Bloomberg"},"_links":{"self":[{"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/posts\/20637","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/users\/184"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/comments?post=20637"}],"version-history":[{"count":5,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/posts\/20637\/revisions"}],"predecessor-version":[{"id":20859,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/posts\/20637\/revisions\/20859"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/media\/18805"}],"wp:attachment":[{"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/media?parent=20637"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/categories?post=20637"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bloomberg.com\/company\/wp-json\/wp\/v2\/tags?post=20637"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}