<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergey Nikitin</title>
    <description>The latest articles on DEV Community by Sergey Nikitin (@n0th1ng_else).</description>
    <link>https://dev.to/n0th1ng_else</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F597844%2F22fba514-fd94-4c54-840b-ba95a808b9f0.jpeg</url>
      <title>DEV Community: Sergey Nikitin</title>
      <link>https://dev.to/n0th1ng_else</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/n0th1ng_else"/>
    <language>en</language>
    <item>
      <title>We migrated 70,000 tests from Jest to Vitest. Yes, in 2 months</title>
      <dc:creator>Sergey Nikitin</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:05:24 +0000</pubDate>
      <link>https://dev.to/n0th1ng_else/we-migrated-70000-tests-from-jest-to-vitest-yes-in-2-months-3lmn</link>
      <guid>https://dev.to/n0th1ng_else/we-migrated-70000-tests-from-jest-to-vitest-yes-in-2-months-3lmn</guid>
      <description>&lt;p&gt;YOU got it right. We migrated 70,000 tests (reads: 70 thousand) from Jest to Vitest. The logical question would be: how? And here is where the story begins.&lt;br&gt;&lt;br&gt;
I work as a software engineer in the Frontend Developer Experience team at &lt;a href="https://miro.com" rel="noopener noreferrer"&gt;Miro&lt;/a&gt;. Our ownership covers various parts of the platform. One of the major focuses for us is frontend infrastructure and tooling – everything that makes the product teams at Miro fast and productive. 2026 was indeed a pivotal year for us: we were finally able to kill Angular.js (I know, right), switched from Prettier to Oxfmt, got rid of ESLint in favor of Oxlint, we are running the CI typechecking with TypeScript 7 and much more. But look, you are here to read about unit-tests! In this article, I will cover how we migrated the main Miro frontend application unit testing harness from Jest to Vitest.&lt;/p&gt;
&lt;h2&gt;
  
  
  The beginning
&lt;/h2&gt;

&lt;p&gt;The main Miro application is constantly evolving and sits now at ~6 million LoC. At this scale, the regular approaches and tooling stop functioning. How can I tell? Here are some facts about our CI state, unit-test jobs specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We run 6 CI runners in parallel &lt;strong&gt;just for unit-tests&lt;/strong&gt;. Each runner takes up to 20 minutes to finish&lt;/li&gt;
&lt;li&gt;For Jest 30 the average speed is ~13 tests per second, even with &lt;a href="https://swc.rs/docs/usage/jest" rel="noopener noreferrer"&gt;SWC&lt;/a&gt; as the code parser&lt;/li&gt;
&lt;li&gt;25% of all runs are failing because of memory leaks, i.e. the CI runners are dying with OOM errors&lt;/li&gt;
&lt;li&gt;We had to apply specific patches for Jest core packages to maintain a better worker pool
(&lt;a href="https://github.com/jestjs/jest/issues/14818" rel="noopener noreferrer"&gt;were never accepted upstream&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Migrating from Jest 29 to Jest 30 (yes, the first major release in 3 years) gave us only around 5% speed and memory improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not look bright, and it has a big impact on the new releases for our application. But what could we do about that?&lt;/p&gt;

&lt;p&gt;Lurking through the Slack messages, I found a few threads where we actually discussed if we want to migrate away from Jest. Back in November 2023, one of my colleagues wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Although I’ve spent some time also exploring an alternative (vitest) but I’ve found that in practice the extra set of vitest specific configurations and replicating our current build might be too risky, without knowing how much speed benefits we get now that we’re relying on @swc/jest&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At that moment, the scale of the problem was manageable – we just started to run the unit-tests in parallel on CI, compensating for the waiting time with the compute power from extra runners. Worked pretty well.&lt;/p&gt;

&lt;p&gt;In July 2024, another colleague of mine made a remark regarding the unit-test tooling one more time:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I expect higher returns from better test selection (e.g. not running 35k unit tests each time). The cost of switching to vitest (for example) is high and unclear what the improvement is&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair enough! 6 runners for 35,000 tests in parallel took just around 7 minutes each. With job durations like this, we had much bigger problems to solve, think of end-to-end tests, for example.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Fast-forward to March 2025. We started facing more and more issues with the Jest stack for testing. Let me give just one example that triggered me to explore the alternatives to Jest. Apparently, our engineers reported that with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest" rel="noopener noreferrer"&gt;VSCode Jest extension&lt;/a&gt;, multiple versions of &lt;a href="https://www.npmjs.com/package/fb-watchman" rel="noopener noreferrer"&gt;fb-watchman&lt;/a&gt; (Jest’s in-house filesystem watcher library) were installed, which led to CPU saturation and, as a result, the inability to use their laptops. We figured out how to fix it with &lt;a href="https://jestjs.io/docs/configuration#watchman-boolean" rel="noopener noreferrer"&gt;watchman: false&lt;/a&gt;. At that moment, Ahmed, a Principal engineer in the Frontend Developer Experience team, had enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want to say maybe it's time to switch to vitest, but that's a massive effort…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We had a conversation regarding this, but I had no capacity to lead the migration right away. We also saw a lot of traction regarding the AI models and how they rapidly evolve for more and more complex problems. All signs were to wait for a bit. And this is how we landed in 2026.&lt;/p&gt;

&lt;p&gt;Now you may ask: “Sergey, why did you decide to explore the migration again, considering all the facts you just mentioned?” Don’t get me wrong, we are not solving problems of this scale just because it’s cool. We need to understand what we want to achieve. I will start from the facts first:&lt;/p&gt;

&lt;p&gt;The main Miro application is a monorepo, that includes both web and native parts. It is managed by &lt;a href="https://yarnpkg.com/" rel="noopener noreferrer"&gt;yarn&lt;/a&gt; and &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;NX&lt;/a&gt;. At the beginning of 2026, we had 800+ packages in our codebase. The largest code share is located in the web core, which accounts for around 20% of the codebase. We also had ~70,000 unit-tests (doubled in 2 years, imagine that) and the unit-tests jobs were taking up to 20 minutes on average to finish, which made them the slowest CI jobs in our pipeline, even worse than building the application and running the e2e tests on the new version!&lt;/p&gt;

&lt;p&gt;Now, we can already see a few pain points we are fighting against. I can compile the whole list for you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CI runners memory limit. 25% of the runs are failing because of memory leaks&lt;/li&gt;
&lt;li&gt;Even with the SWC parser, Jest is slow, at 13 tests/sec for us (we use JSDOM and canvas mock for testing)&lt;/li&gt;
&lt;li&gt;The TypeScript compatibility is not excellent. There are still API methods that infer the &lt;code&gt;any&lt;/code&gt; type.&lt;/li&gt;
&lt;li&gt;There is no proper ESM support, no mock hoisting. We add the spying code and mock factories between the top-level imports, so the import statements are downleveled to &lt;code&gt;require()&lt;/code&gt; and can pick the mocked implementation.&lt;/li&gt;
&lt;li&gt;Because of the previous point, we still transform the code to CommonJS. This means we are not testing what we are delivering to our customers (we deliver the ESM bundles).&lt;/li&gt;
&lt;li&gt;The test coverage report is inaccurate. The packages do not share the instrumentation outside of each project root directory. This effectively means that there is missing cross-package coverage.&lt;/li&gt;
&lt;li&gt;No test isolation. The jest workers in the pool are not spawned for each module; rather, they are recycled once a worker hits the memory limit.&lt;/li&gt;
&lt;li&gt;There are no guardrails for the test fixtures to prevent leaking into the production bundle (funny spoiler: some teams actually used the test fixtures in the production code, intentionally)&lt;/li&gt;
&lt;li&gt;Let's be real, it took Jest 3 years to release a new major version. We have opened a few issues on GitHub, but we did not see any traction around it.&lt;/li&gt;
&lt;li&gt;Not only us, did I mention the incompatibility with other popular libraries?
&lt;a href="https://github.com/jestjs/jest/issues/15816" rel="noopener noreferrer"&gt;Prettier, for example&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I can go on and on, but I think this is already impressive.&lt;br&gt;&lt;br&gt;
Now, you cannot just take 70k tests and rewrite them with a random testing framework (imagine you could!). You also have to educate hundreds of product engineers, and the scale of the change would be massive. And also, how about all the custom plugins, matchers, and supporting dependencies? Think of &lt;code&gt;jest-axe&lt;/code&gt;, &lt;code&gt;jest-when&lt;/code&gt;, React Testing Library, and others. The answer to all of this is – we are looking for a Jest-compatible api. Compatible enough so we won’t turn our monorepo into chaos.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where to migrate?
&lt;/h2&gt;

&lt;p&gt;Now, the market research gave us two alternatives: Rstest and Vitest (versions as of March 2026):&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;RStest v0.9.8 (&lt;a href="https://rstest.rs/" rel="noopener noreferrer"&gt;https://rstest.rs&lt;/a&gt;)&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Part of &lt;a href="https://rspack.rs/" rel="noopener noreferrer"&gt;Rstack toolchain&lt;/a&gt;, so we can reuse the Rspack configuration from our application&lt;/li&gt;
&lt;li&gt;Modern ESM-based syntax (think of
&lt;a href="https://rstest.rs/api/runtime-api/rstest/mock-modules#partial-mock-with-importactual" rel="noopener noreferrer"&gt;import attributes&lt;/a&gt; and more explicit mocking syntax)&lt;/li&gt;
&lt;li&gt;Integration with Rsdoctor for debugging&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;Vitest v4.1.6 (&lt;a href="https://vitest.dev/" rel="noopener noreferrer"&gt;https://vitest.dev&lt;/a&gt;)&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Wide community adoption&lt;/li&gt;
&lt;li&gt;Type safety and ESM-first&lt;/li&gt;
&lt;li&gt;Test isolation and rich, comprehensive api&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can guess Vitest won, but why? I did my homework: I created two branches and migrated around 10k tests to each of the frameworks. Then I tried to execute the tests in different modes. Here are the results (the values are tests per second, the bigger, the better):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Jest&lt;/th&gt;
&lt;th&gt;Vitest&lt;/th&gt;
&lt;th&gt;Rstest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local run&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14.7 t/sec&lt;/td&gt;
&lt;td&gt;16.8 t/sec&lt;/td&gt;
&lt;td&gt;7.6 t/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local run with coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.9 t/sec&lt;/td&gt;
&lt;td&gt;8 t/sec&lt;/td&gt;
&lt;td&gt;2.9 t/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI run&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12.9 t/sec&lt;/td&gt;
&lt;td&gt;13 t/sec&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI run with coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 t/sec&lt;/td&gt;
&lt;td&gt;8.6 t/sec&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can already notice that Rstest failed to run on CI, mainly because of bad memory management in the worker pool. Keep in mind that it was pre-1.0 version and the runner is under active development, so the hopes were low (I encourage you to try the latest version and explore the current state of things). In the meantime, migrating this small test subset showed a huge difference in memory consumption for Vitest – more than 10GB less memory used on the runner compared to the same group of tests in Jest 🚀.&lt;/p&gt;

&lt;p&gt;Just to be honest here, the speed and RAM consumption were indeed among the main factors. And although the research did not show any meaningful speed improvements, we decided to execute the migration as we saw opportunities to optimize Vitest configuration and try to make it faster. It also covers other pain points we were targeting to solve. Yes, we have a front-runner, and we are now coming to a plan!&lt;/p&gt;
&lt;h2&gt;
  
  
  The plan
&lt;/h2&gt;

&lt;p&gt;As I said already, the Miro web application is a monorepo orchestrated by NX and Yarn. It combines the web core monolith, holding ~20% of all the code. Other parts are: 800+ packages and the part for native applications. And since the native part has its own CI and release pipeline, we decided to leave it out of scope.&lt;br&gt;&lt;br&gt;
On a separate note, the application accepts 60+ merges into the main branch on a daily basis. And as much as I wanted to “just” ask the AI agents to migrate everything in one go, it was not possible for us to execute. I decided to go with a gradual rollout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run both Jest and Vitest on separate runners on CI in parallel. As the migration goes, reallocate the runners depending on the share of migrated tests&lt;/li&gt;
&lt;li&gt;Start with packages. This way, we will be able to deliver the meaningful parts, and it is easy to discover which tests should go to Jest and which to Vitest.&lt;/li&gt;
&lt;li&gt;Use AI agents to migrate the tests. We create a skill for the agents in the repo, so the agents have all necessary context. After each migration phase, we update our patterns not mentioned in the skill files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last bit is the most powerful. Now, the agents learn and remember new patterns. This makes the migration faster and produces fewer hallucinations with each iteration. This also makes the migration more consistent – the agents apply the same patterns they saw before. Not only that, once completed, you will end up having a huge list of very specific code examples the agents found in the tests – an amazing source of insights to reflect on. It can easily open the room for follow-ups, good practices, and knowledge sharing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 1 - Infrastructure
&lt;/h3&gt;

&lt;p&gt;I started with infrastructure for Vitest. The basics include having the very first version of Vitest config to reuse in the application.&lt;br&gt;&lt;br&gt;
Then we added the compatibility bridges. Think about shared mocks and helpers that create the fake data. During the time of migration, we do not want to duplicate such files. Hence, we added the shim &lt;code&gt;globalThis.jest = vi&lt;/code&gt; in Vitest config. And &lt;code&gt;globalThis.vi = jest&lt;/code&gt; in Jest. Yes, the api is not 100% compatible, but this was enough in our case.&lt;br&gt;&lt;br&gt;
Now the whole idea of migration was built around a gradual switch package by package. Each package runs the scripts via the CLI commands, think of it as having a command &lt;code&gt;miro-scripts test&lt;/code&gt;. It took me to add the runner’s opt-in argument, so we can distinguish which test runner to spawn. Looks like this: &lt;code&gt;miro-scripts test --runner=vitest&lt;/code&gt; in the package.json file.&lt;br&gt;&lt;br&gt;
It was not just a local story. This flag allowed us to see how to run these tests on CI runners. As we specialized the runners per tool, we combined all Vitest projects into one pool of runners, and the rest was for the remaining Jest projects.&lt;br&gt;&lt;br&gt;
Finally, we adjusted the observability metrics in order to track the progress and test distribution. We added the extra &lt;code&gt;runner&lt;/code&gt; label for test-related Prometheus metrics, which made it possible to visualize the migration.&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 2 - First steps
&lt;/h3&gt;

&lt;p&gt;Now we were all set to start the actual migration. All guardrails were there, the CI picked up all the tests, so we were not losing quality. The only missing part was the test coverage for Vitest, but we agreed to handle it after the transition.&lt;br&gt;
I started with small packages. This allowed us to keep the pull-requests size manageable. This way I could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Review the changes more precisely (as I said, in the later stages, the built context was so rich that it could hardly produce off-track changes)&lt;/li&gt;
&lt;li&gt;Form the foundation of the AI skill. First instructions, patterns, and polyfills were missing in the config&lt;/li&gt;
&lt;li&gt;Convince teams and build trust. We, as a Frontend Developer Experience team, knew we needed to modernise the tech stack. But it was not obvious for the product teams. I made sure to be vocal about the migration and tried to iterate on the reasoning behind the change&lt;/li&gt;
&lt;li&gt;Start collecting issues. For example, there were &lt;a href="https://github.com/firsttris/vscode-jest-runner" rel="noopener noreferrer"&gt;some extensions in VSCode&lt;/a&gt; that tried to guess the test framework used in the repo by the root-level config. With both Jest and Vitest configs at the root, it gave priority to Vitest, which at that time had only a fraction of packages with completed migration – just one of those things that I could only assume at the beginning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the first version of the AI skill was ready, I made sure to focus on the community work, presenting on it internally and explaining how each team could DIY their own migration. The skill is in the repo, just ask your agent to migrate the packages under your ownership! ✨&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 3 - Goes well
&lt;/h3&gt;

&lt;p&gt;We started off strong with the migration. The first 90 packages went pretty well. At this moment, we built the needed Vitest plugins to cover parts of Jest configuration and added a decent number of global polyfills. I intentionally did not ask the AI agents to mirror the Jest config – nobody touched it for years, and it absorbed so much legacy, I did not want to port it to Vitest and started from scratch. I also tried to run the migration based on ownership, i.e. in a single pull-request I would only transform tests for the single owning team. Unfortunately, this did not work as expected. We have so many cross-package helpers and fixture generators, so it was never a single team, rather a few owners here and there. But that was still acceptable to us.&lt;br&gt;
At that point, I felt so confident that I decided to take one of the biggest packages in our repo (apart from the web monolith). ~600 test files all at once, a true battle test for the migration flow. BOOM, we landed it in the main branch! It was a huge milestone and proof we could continue.&lt;br&gt;
On another side, we kept working with the frontend community and were very transparent about the effort. As a result, we started receiving positive feedback about the progress in general and how smoothly the migration was going.&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 4 - It couldn’t be &lt;em&gt;that&lt;/em&gt; easy
&lt;/h3&gt;

&lt;p&gt;Although the mechanical migration was progressing well, our responsibility never changed – stable CI processes. Not only did we need to migrate the code, but we also needed to follow the metrics. Unfortunately, the metrics started showing a coming disaster.&lt;br&gt;
The CI runner started consuming more and more memory. I was confused! My initial research gave me the signal that Vitest consumed much LESS memory, but we were around 50% of the way through the migration, and it was getting only worse?! We had to pause everything until we figured it out. Imagine my mental state at that time: we were in the middle of a transition. I couldn’t roll everything back, but I also could not move on. What do I do now? Did I fail?!&lt;br&gt;
I was buried in the problem for days until I came to the realization:&lt;br&gt;
Each package in the repository is defined as a &lt;a href="https://vitest.dev/guide/projects.html" rel="noopener noreferrer"&gt;Vitest project&lt;/a&gt; (for simplicity, I will use the terms package and project interchangeably). Turns out, &lt;a href="https://github.com/vitest-dev/vitest/discussions/10572" rel="noopener noreferrer"&gt;Vitest projects do not share the transformation cache&lt;/a&gt;! Every project builds up the cache with a unique identifier under the hood. If packages are heavily interconnected, Vitest spawns a Vite server for each package and transforms the same module graph again and again. So 10 linked packages will transform the same modules 10 times. Now scale it up to 800 packages, and you will get it. And in my initial research, I only used the subset of the tests inside a single Vitest project, thus, I could not actually see it coming.&lt;br&gt;
The solution is to run fewer projects, not by ignoring some of them, but rather by merging the projects into a single instance. Fortunately, most of the packages in our application have the default shared Vitest config with no overrides. So it was easy to write a project discovery script that would combine all packages with default configuration into a single project. By doing this, we were able to run only 60 Vitest projects that actually account for 400 packages (59 custom projects + 1 project that includes the remaining files).&lt;br&gt;
I will give you a sneak peek with the numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;th&gt;% Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heap Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8,770 MB&lt;/td&gt;
&lt;td&gt;5,024 MB&lt;/td&gt;
&lt;td&gt;-3,746 MB&lt;/td&gt;
&lt;td&gt;-42.71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21,403 MB&lt;/td&gt;
&lt;td&gt;18,564 MB&lt;/td&gt;
&lt;td&gt;-2,839 MB&lt;/td&gt;
&lt;td&gt;-13.26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1059.83s&lt;/td&gt;
&lt;td&gt;611.06s&lt;/td&gt;
&lt;td&gt;-448.77s&lt;/td&gt;
&lt;td&gt;-42.34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transform Time&lt;/td&gt;
&lt;td&gt;10756.03s&lt;/td&gt;
&lt;td&gt;4327.69s&lt;/td&gt;
&lt;td&gt;-6428.34s&lt;/td&gt;
&lt;td&gt;-59.76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup Time&lt;/td&gt;
&lt;td&gt;1495.36s&lt;/td&gt;
&lt;td&gt;1277.27s&lt;/td&gt;
&lt;td&gt;-218.09s&lt;/td&gt;
&lt;td&gt;-14.58%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import Time&lt;/td&gt;
&lt;td&gt;13452.26s&lt;/td&gt;
&lt;td&gt;6984.71s&lt;/td&gt;
&lt;td&gt;-6467.55s&lt;/td&gt;
&lt;td&gt;-48.08%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests Time&lt;/td&gt;
&lt;td&gt;257.29s&lt;/td&gt;
&lt;td&gt;262.76s&lt;/td&gt;
&lt;td&gt;+5.47s&lt;/td&gt;
&lt;td&gt;+2.13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment Time&lt;/td&gt;
&lt;td&gt;344.18s&lt;/td&gt;
&lt;td&gt;371.06s&lt;/td&gt;
&lt;td&gt;+26.88s&lt;/td&gt;
&lt;td&gt;+7.81%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Curious readers may ask: why did Environment time and Test time increase? Imagine you run 10 Vitest workers in parallel. And only a single main process that runs Vite to transform the import tree. Before the change, the workers would just hang and wait when the main process transforms the same modules again and again. Once we merged the projects, the transformation time dropped significantly. Now the workers do not wait, they run more tests, which causes CPU saturation and system pressure in general. This effectively results in slower worker performance. But since the actual test run takes only a fraction of the overall test execution process, the degradation is negligible.&lt;br&gt;&lt;br&gt;
That being said, combining all standard projects into a single instance made an outstanding impact on the performance and &lt;a href="https://github.com/vitest-dev/vitest/discussions/10572" rel="noopener noreferrer"&gt;confirmed my assumption&lt;/a&gt;. The most important thing is that we can now continue with migration!&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 5 – Closer to the goal
&lt;/h3&gt;

&lt;p&gt;The next 60 pull-requests were just mechanical. The AI agent has built up the comprehensive skill context. My job was to check the bad-looking and non-trivial changes. Then also hear the feedback from the reviewers, since I cannot in this world know each and every unit-test in the Miro application.&lt;br&gt;
One nice cherry on top was the fact that with the Jest → Vitest migration, we started noticing some test fixtures that had accidentally leaked into our production bundle (remember point 8?). The logical question would be: how? How did we detect this, and how did the test data leak into the production bundle?&lt;/p&gt;

&lt;p&gt;The first part was easy for us – we build the web application with Rspack with no overrides or patches in the config to support Node.js modules. In fact, Vitest uses the NodeJS api under the hood (it is a Node library in the end). As a result, Rspack would fail to build the application once any part of Vitest is imported in the dependency graph.&lt;/p&gt;

&lt;p&gt;How does it usually happen for us?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Barrel files. Every package in the repository exports its API via the barrel file &lt;code&gt;index.ts&lt;/code&gt;. Although it would work fine in small projects, barrel files give nothing but pain as the application gets bigger. Barrel files, and especially star exports (&lt;code&gt;export * from ‘./foo.ts’&lt;/code&gt;) are hard to tree-shake, and you never know what exactly is being exported. Accidentally, some test shapes were exported transitively as well.&lt;/li&gt;
&lt;li&gt;Some teams actually used test helpers to create artificial users and accounts in the production code. These were edge cases but still happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Vitest migration clearly highlighted such cases and made us improve the codebase. This gave us approximately a 4MB cut from the full production build.&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 6 - One-shotting the web core
&lt;/h3&gt;

&lt;p&gt;We had migrated all packages at that point, and the AI agents now had a definitive playbook on how to run the migration. Now it was time for the final boss: the web core monolith. 1,800 test files, 16,500 tests. I realized it was too risky to run the migration during business hours – engineers merge pull-requests once every 10 minutes. I would spend more time resolving the conflicts. I decided to handle it during the weekend (no worries, I took two days off to compensate!)&lt;br&gt;
On Saturday, I told the AI agent to run the migration. The result: 1,985 files changed, +22,776 / -19,998 lines diff. This pull-request tagged each and every team for review, but my plan was to merge it right away with an admin override approval. It wasn’t perfect, but it was decent enough. The rest we could fix later with smaller PRs during business hours. It took me an entire day to look at the changelog!&lt;br&gt;
I clicked the &lt;code&gt;Bypass rules and merge&lt;/code&gt; button and waited for the main branch CI to complete. I could finally go to sleep. &lt;strong&gt;We made it&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Act 7 – Aftermath breakthrough
&lt;/h3&gt;

&lt;p&gt;I don’t want to give the impression that the transition was perfectly smooth. Once we completed the mechanical work, we had to clean up the configs, revert the hybrid setup, and start removing Jest dependencies. But this is fine. The bigger problem (ironically) was that Vitest is a much more reliable tool than Jest. It has its own timeout for module imports, it catches unhandled errors, it races the duplicated vi.mock blocks. All of this actually made a small subset of tests more flaky. We spent some time stabilizing the tests, exploring the actual cause, and working with the owners to see how we could address it. A fractional chunk of tests (think of 27 test cases) was completely skipped for the sake of CI stability.&lt;/p&gt;

&lt;p&gt;But one question still stood out in my mind – we were able to speed up the run and save memory by merging the projects. Could we merge &lt;strong&gt;everything&lt;/strong&gt; into a single Vitest project?&lt;br&gt;
One of the advantages of the transition to Vitest is that we were able to drop tons of legacy configurations. This made all custom Vitest configs look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineProject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mergeConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defaultCfg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@shared/config/vitest/shared.config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;mergeConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;defaultCfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;defineProject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./setupVitest.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the setup script is what makes us create the custom configs. Why does that matter? If the setup file is the only difference, maybe I can detect which test file is running and call the setup script relative to that file!&lt;br&gt;&lt;br&gt;
There is no documentation around it. The only hint I got is &lt;a href="https://vitest.dev/api/expect.html#expect" rel="noopener noreferrer"&gt;this page&lt;/a&gt;, which explains the interface for the &lt;code&gt;expect&lt;/code&gt; api. Apparently, &lt;code&gt;expect.getState: () =&amp;gt; MatcherState&lt;/code&gt;, and then &lt;code&gt;MatcherState&lt;/code&gt; has the field &lt;code&gt;testPath&lt;/code&gt; - the absolute test path, which is &lt;strong&gt;exactly what we are looking for&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Now I quickly spun up the shared setup script, which takes the test path, derives the package the test is located in. and then dynamically executes the setup script attached to that specific test file. Here is the idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoRootDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%repo_root_path%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absoluteTestPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relativeTestPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;absoluteTestPath&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REPO_ROOT_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;absoluteTestPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// you can manipulate the path to find the right location here&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vitestProjectRootDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchProjectDirForTestFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relativeTestPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setupFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vitestProjectRootDir&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRootDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vitestProjectRootDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;setupVitest.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setupFile&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setupFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setupFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can make this script project-agnostic and execute it for each test file. Doing this, we were able to merge ALL 800+ Vitest projects in our application into a single test run. No transformation overhead, no cache duplications. To give you an idea of how impactful it was, I will give you a sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;th&gt;% Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heap Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5,238 MB&lt;/td&gt;
&lt;td&gt;1,857 MB&lt;/td&gt;
&lt;td&gt;-3,381 MB&lt;/td&gt;
&lt;td&gt;-65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;22,460 MB&lt;/td&gt;
&lt;td&gt;17,450 MB&lt;/td&gt;
&lt;td&gt;-5,010 MB&lt;/td&gt;
&lt;td&gt;-22.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;865.64s&lt;/td&gt;
&lt;td&gt;588.82s&lt;/td&gt;
&lt;td&gt;-276,82s&lt;/td&gt;
&lt;td&gt;-32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12.6 t/sec&lt;/td&gt;
&lt;td&gt;19 t/sec&lt;/td&gt;
&lt;td&gt;+6.4 t/sec&lt;/td&gt;
&lt;td&gt;+50%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Finally, we found the source of the speed ✨ And this concluded our road to landing Vitest in our main Frontend application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Active migration time&lt;/th&gt;
&lt;th&gt;About 80 days (May 4 to July 20)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merged pull requests&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines added/removed&lt;/td&gt;
&lt;td&gt;+87,701 / -78,946&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Net lines&lt;/td&gt;
&lt;td&gt;+8,755&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files touched&lt;/td&gt;
&lt;td&gt;9,328&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice we have added almost 9,000 net lines of code. The actual net is much lower, because we moved from globally injected api (think &lt;code&gt;jest&lt;/code&gt;, &lt;code&gt;it&lt;/code&gt;, &lt;code&gt;describe&lt;/code&gt;, etc.) to locally imported objects (&lt;code&gt;import {describe} from ‘vitest’&lt;/code&gt;). Plus, code formatting gave some diff. Now to the technical details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The test runs are 50% faster than they were with Jest (13 t/sec to 19 t/sec)&lt;/li&gt;
&lt;li&gt;Vitest consumes 30% less memory on the CI runners&lt;/li&gt;
&lt;li&gt;Vitest mocks are type-safe i.e. they inherit the signature from the mocked object, allowing engineers to write better unit-tests.&lt;/li&gt;
&lt;li&gt;ESM-first. The mocks are hoisted automatically, which gives again the better experience for the engineers.&lt;/li&gt;
&lt;li&gt;Production bundle saved around 4MB, the test fixtures were pulled out from the production barrel files&lt;/li&gt;
&lt;li&gt;Real test isolation – each test file is being run on an isolated worker in the pool, ensuring tests do not interfere with each other&lt;/li&gt;
&lt;li&gt;Accurate test coverage. &lt;a href="https://vitest.dev/config/coverage.html#coverage-allowexternal" rel="noopener noreferrer"&gt;allowExternal: true&lt;/a&gt; allows us to collect the test coverage, dropping the project boundary. We collect the full picture.&lt;/li&gt;
&lt;li&gt;We collected the test polyfills from all files, generalised them, and stored them in a shared configuration. Not only did it simplify the tests, but we also made sure engineers have a consistent experience with the polyfilled api’s&lt;/li&gt;
&lt;li&gt;The migration AI skill was transformed into the unit-tests creation skill. Now it gives the agents the common patterns from our codebase, which enabled better test generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The total model usage during this period cost around $7,000. To put this into perspective, I estimated the annual savings from the migration on CI runners at approximately $75,000. We should also account for more application releases: as I mentioned at the beginning, we started facing more and more memory issues for Jest runs. Add here the faster feedback loop during the local development as well, which is, to be fair, hard to estimate. All of this made the investments a huge win for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;In 2026 here at Miro, we keep experimenting with large-scale migrations. Our goal is to provide engineers with the best-in-class tooling and infrastructure. We have already made a transition from Prettier to Oxfmt, from ESLint to Oxlint, from Webpack to Rspack. It was just about time to switch from Jest to Vitest. Each journey results in lots of artifacts for future improvements. And lessons learned along the way. Here are some of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set a clear goal&lt;/strong&gt;. Be specific about what you want to accomplish. Research the market and find the tools that can move you towards the goal. Experiment with a subset of the codebase to be able to estimate the scale and feasibility, and to get a feel for the planned work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan the execution&lt;/strong&gt;. Sometimes you just need to make a big bang migration. Sometimes you can be more gentle and have a gradual rollout. For Vitest, I made a safe bet to iterate and build up the context for the AI agents. It worked pretty well, and at some point we were able to one-shot the remaining tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be specific in the prompts with AI agents&lt;/strong&gt;. Give a deterministic way to reproduce the result. Set the constraints. It was not possible to run the subagents in parallel because Vitest takes so much memory in our case. I asked to run the migration sequentially, so it did not ruin my workstation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build trust&lt;/strong&gt;. People resist change. My goal was to give the reasoning behind the change, acknowledge the struggle to understand where to use which runner, and help to adopt the new tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can not plan for everything&lt;/strong&gt;. There are always issues you could not possibly expect during the preparation. You need to accept it and not be afraid to pivot and look up for the solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scale of change does not matter at this point&lt;/strong&gt;. With Fable 5, or Sol, AI can execute any volume of change. If you can formalise what needs to be done, if you can create a context for the agent, if you can give a deterministic reproduction – the implementation part is extremely democratised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust the CI&lt;/strong&gt;. Some changes during the migration were trivial. Others touched the production logic. I worked with the code owners to understand if the change I am introducing is legit. But at the end of the day, you have to build the release quality gates that are trustworthy, and a rollback process so fast, so even if you end up having a bad day, you can easily fix the mistake.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  To wrap it up
&lt;/h2&gt;

&lt;p&gt;We have completed the migration of our main frontend application from Jest to Vitest. The transition took 70,000 tests to transform (75,000 as of the last merged pull-request date). It took us just a bit over 2 months to execute. We used Opus 4.8 and Fable 5 (once it was released) to drive the migration.&lt;br&gt;&lt;br&gt;
Migration to Vitest gave us space and resources to grow further, modernised our codebase, and allowed us to start writing type-safe unit-tests. It also caught the test data leaks into the production bundle and actual production bugs.&lt;br&gt;&lt;br&gt;
This project paid off in all areas we were targeting. It also built a story of how useful the AI agents can be if done right.&lt;/p&gt;

&lt;p&gt;Special credits go to &lt;a href="https://www.linkedin.com/in/evgeny-evsyukhin" rel="noopener noreferrer"&gt;Evgeny Evsyukhin&lt;/a&gt;, who has heavily contributed to the migration as well as the observability infrastructure around Vitest. I want to also mention &lt;a href="https://www.linkedin.com/in/ivan-voronin" rel="noopener noreferrer"&gt;Ivan Voronin&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/ahmedelgabri" rel="noopener noreferrer"&gt;Ahmed El Gabri&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/fredericoestrela" rel="noopener noreferrer"&gt;Frederico Estrela&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/viniciuskneves/" rel="noopener noreferrer"&gt;Vinicius Kiatkoski Neves&lt;/a&gt;, and my beloved Frontend Developer Experience team for making the migration possible. I also want to give a shoutout to &lt;a href="https://www.linkedin.com/in/ben-makuh" rel="noopener noreferrer"&gt;Ben Makuh&lt;/a&gt; for proofreading and helping me refine this article.&lt;/p&gt;

&lt;p&gt;PS: Since we created this Jest to Vitest migration skill, I thought it would be nice to create an open-source version as well. It can be a good foundation for your own contextual migration skill. &lt;a href="https://github.com/n0th1ng-else/agent-skills#jest-to-vitest-migration-skill" rel="noopener noreferrer"&gt;Feel free to use and contribute here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jest</category>
      <category>vitest</category>
      <category>monorepo</category>
      <category>ai</category>
    </item>
    <item>
      <title>Axios timeout for chunked responses</title>
      <dc:creator>Sergey Nikitin</dc:creator>
      <pubDate>Wed, 07 Oct 2026 22:02:33 +0000</pubDate>
      <link>https://dev.to/n0th1ng_else/axios-timeout-for-chunked-responses-39j4</link>
      <guid>https://dev.to/n0th1ng_else/axios-timeout-for-chunked-responses-39j4</guid>
      <description>&lt;p&gt;ALRIGHT, this was an interesting one. I use &lt;a href="https://axios-http.com" rel="noopener noreferrer"&gt;Axios&lt;/a&gt; in one of &lt;a href="https://github.com/n0th1ng-else/voice-to-text-bot" rel="noopener noreferrer"&gt;my pet projects&lt;/a&gt;. Basically, the project is a Telegram bot that transforms voice messages into text. I know that Telegram now has this feature for premium subscribers, but this is not something we are about to cover in this article. Here I want to talk about the tricky way the Axios timeout works.&lt;/p&gt;

&lt;p&gt;Let's start with the simple HTTP server (I use Express as an example) and a single API endpoint, which takes 10 seconds to resolve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// I have an API endpoint which takes 10s to resolve&lt;/span&gt;
&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/regular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we deep dive into the &lt;a href="https://axios-http.com/docs/req_config" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, we will find that Axios supports the &lt;code&gt;timeout&lt;/code&gt; property, and it works quite well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Start request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/regular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request was successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Request took &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s,`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should be 5s!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Start request&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// AxiosError: timeout of 5000ms exceeded&lt;/span&gt;
&lt;span class="c1"&gt;//     at RedirectableRequest.handleRequestTimeout (/axios-streaming-timeout/node_modules/axios/lib/adapters/http.js:630:16)&lt;/span&gt;
&lt;span class="c1"&gt;//     at RedirectableRequest.emit (node:events:513:28)&lt;/span&gt;
&lt;span class="c1"&gt;//     at Timeout.&amp;lt;anonymous&amp;gt; (/axios-streaming-timeout/node_modules/follow-redirects/index.js:169:12)&lt;/span&gt;
&lt;span class="c1"&gt;//     at listOnTimeout (node:internal/timers:569:17)&lt;/span&gt;
&lt;span class="c1"&gt;//     at process.processTimers (node:internal/timers:512:7) {&lt;/span&gt;
&lt;span class="c1"&gt;//   code: 'ECONNABORTED',&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Request took 5s, should be 5s!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If it works fine, what is the problem then, you ask me. Here is the thing: as I said, I have a Telegram bot that converts voice messages into text. It basically decompresses the voice file into a WAV buffer and sends the data to one of the cloud providers. As a result, I receive text recognition for that specific voice message.&lt;/p&gt;

&lt;p&gt;In fact, one of the providers I use implements the streaming API. The intention is to show the text recognition for the user in real time while it gets processed (Which is quite cool, huh). You receive chunks of text data, and you have a choice to wait until you can combine the whole message, or send it out to the user so they see the early results. On a code level, it actually resolves the request promise, so you can handle the chunk of data. With the following chunks, it is being resolved again and again until the final chunk is received and the request is finished.&lt;/p&gt;

&lt;p&gt;If you read this, you can already see the unexpected side effect of this approach. Axios resolves the request promise with the first chunk received. Essentially, this means that Axios resets the timeout, and the request time is not controlled in any way. For example, I have a timeout of 10 seconds for my bot recognition engine. And I could see in the logs that requests were taking 30, 45, 50 seconds, and even more. With the first chunk received, Axios stops counting the timeout frame and considers the request resolved. To illustrate this, I created a new endpoint in the Express server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// I now have an API endpoint that sends one chunk of the response each second&lt;/span&gt;
&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Transfer-Encoding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Keep sending chunks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Finalize the request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if I call the new endpoint, I will experience rather unexpected behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Start request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request was successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Request took &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s,`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should be 5s!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Start request&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 1&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 2&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 3&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 4&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 5&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 6&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 7&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 8&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 9&lt;/span&gt;
&lt;span class="c1"&gt;// Finalize the request&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Request was successful&lt;/span&gt;
&lt;span class="c1"&gt;// Request took 10s, should be 5s!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request was supposed to take 5 seconds maximum, but it actually took 10 seconds and was successful in the end. Imagine if it hangs for several minutes! Obviously, users are not in favor of waiting that long for voice recognition, and they stop using the bot. But how can we fix it?&lt;/p&gt;

&lt;h2&gt;
  
  
  AbortSignal to the rescue
&lt;/h2&gt;

&lt;p&gt;At first glance, one solution would be to wrap the request in my custom cancellation function, so once, say, &lt;code&gt;setTimeout()&lt;/code&gt; triggers the handler, we reject the promise and fall into an unhappy flow. BUT. Fortunately, there is a better way! Let me introduce you to &lt;strong&gt;AbortSignal&lt;/strong&gt; and &lt;strong&gt;AbortController&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AbortSignal&lt;/strong&gt; is a built-in JavaScript signal implementation, which is widely adopted to control the execution and cancellation of other APIs in the language. The interface itself is fairly simple. Basically, it only contains a couple of static methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AbortSignal.abort()&lt;/code&gt; returns the instance of AbortSignal which is already aborted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AbortSignal.timeout(timeout: number)&lt;/code&gt; returns the signal that will be aborted in the timeframe you specify in the
parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As I said, the implementation is pretty simple and does not give us much flexibility. For more complex cases, AbortController comes in handy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AbortController&lt;/strong&gt; is a wrapper around the AbortSignal, and it has one huge benefit — the &lt;code&gt;abort()&lt;/code&gt; function. This means you get granular control over the signal execution. You can invoke it manually whenever you need to. Here are some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM event listeners support the AbortSignal, so we can unsubscribe from all the events in one go:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create an abort controller instance. It contains the signal and abort function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;abort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;printBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;print-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;back-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Add the signal to the event listener&lt;/span&gt;
&lt;span class="nx"&gt;saveBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;saveHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Add the signal to another event listener&lt;/span&gt;
&lt;span class="nx"&gt;printBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;printHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Abort the signal when we navigate back&lt;/span&gt;
&lt;span class="nx"&gt;backBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this specific (pretty simplified) example, once we click the &lt;strong&gt;Back button&lt;/strong&gt;, we trigger the &lt;code&gt;abort()&lt;/code&gt; function and hence send out the abort signal. As a result, we have unsubscribed from all three event listeners. Easy as that!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As JavaScript developers, we spend most of the time handling API requests. And the good news here: the &lt;code&gt;fetch()&lt;/code&gt; method supports AbortSignal as well.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create abort controller instance. It contains the signal and abort function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;abort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/regular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;signal&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;back-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Now we abort the fetch request once we click on the back button&lt;/span&gt;
&lt;span class="nx"&gt;backBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request is in the pending state, clicking the Back button will lead to an error in the console &lt;code&gt;DOMException [AbortError]: This operation was aborted&lt;/code&gt;. This is one good example of how to implement the timeout for the &lt;code&gt;fetch()&lt;/code&gt; interface in your application.&lt;/p&gt;

&lt;p&gt;For more information and engine support, you can read the MDN articles about &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal" rel="noopener noreferrer"&gt;AbortSignal&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" rel="noopener noreferrer"&gt;AbortController&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining with Axios
&lt;/h2&gt;

&lt;p&gt;Now that we are familiar with the interfaces we are going to use, I want to draw your attention to one more thing. If we go back to Axios, we will quickly realize that &lt;a href="https://axios-http.com/docs/cancellation" rel="noopener noreferrer"&gt;Axios supports&lt;/a&gt; AbortSignal as well. This was amazing news for me, as I did not need to write my own custom logic around the request cancellation.&lt;/p&gt;

&lt;p&gt;In my implementation, timeout is the only thing that matters, so I can even use AbortSignal directly without having to invoke the AbortController instance — it supports the timeout as I showed above. The fix for my issue is pretty simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Start request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Add the timeout signal in the config&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request was successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Request took &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s,`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should be 5s!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Start request&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 1&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 2&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 3&lt;/span&gt;
&lt;span class="c1"&gt;// Keep sending chunks 4&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// CanceledError: canceled&lt;/span&gt;
&lt;span class="c1"&gt;//     at EventTarget.abort (/axios-streaming-timeout/node_modules/axios/lib/adapters/http.js:192:54)&lt;/span&gt;
&lt;span class="c1"&gt;//     at [nodejs.internal.kHybridDispatch] (node:internal/event_target:737:20)&lt;/span&gt;
&lt;span class="c1"&gt;//     at EventTarget.dispatchEvent (node:internal/event_target:679:26)&lt;/span&gt;
&lt;span class="c1"&gt;//     at abortSignal (node:internal/abort_controller:314:10)&lt;/span&gt;
&lt;span class="c1"&gt;//     at Timeout._onTimeout (node:internal/abort_controller:120:7)&lt;/span&gt;
&lt;span class="c1"&gt;//     at listOnTimeout (node:internal/timers:569:17)&lt;/span&gt;
&lt;span class="c1"&gt;//     at process.processTimers (node:internal/timers:512:7) {&lt;/span&gt;
&lt;span class="c1"&gt;//   code: 'ERR_CANCELED',&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Request took 5s, should be 5s!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have used the static method &lt;code&gt;AbortSignal.timeout(5000)&lt;/code&gt; to cancel the request if it takes longer than 5 seconds. I could do it with AbortController as well, but this requires a couple more lines of code. For instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create an abort controller instance. It contains the signal and abort function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;abort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Abort after 5 seconds&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;signal&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, this approach requires creating an instance of AbortController and creating the timeout manually, so I would recommend using the &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; method directly in such a simple case.&lt;/p&gt;

&lt;p&gt;The last observation to keep in mind is that, since we introduced the AbortSignal, it makes the usage of the Axios &lt;code&gt;timeout&lt;/code&gt; property redundant. The race condition appears to be in favor of AbortSignal all the time. And since it covers the same capability, I believe we can safely remove the &lt;code&gt;timeout&lt;/code&gt; property for good.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/chunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Abort any request by 5 sec timeout&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The outcome
&lt;/h2&gt;

&lt;p&gt;Users hate to see errors in their applications. But even more, they hate to feel the application is not responding, stuck in some in-between state, and does not give feedback if something goes wrong. I can not control the time the cloud provider takes to transform the audio buffer into text, so I show the error message when it takes longer than a reasonable time.&lt;/p&gt;

&lt;p&gt;Axios timeout property works just as well with a regular API. But if you face the streaming API endpoint and want to limit the request execution time, keep in mind that the timeout property won't work as you would expect. One of the options to solve this is to try using the native JS AbortSignal implementation to ensure the timeout is applied for any kind of request. In the meantime, I have filed the &lt;a href="https://github.com/axios/axios/issues/5886" rel="noopener noreferrer"&gt;issue report&lt;/a&gt; in the Axios repository, asking to clarify the timeout behavior in the documentation.&lt;/p&gt;

&lt;p&gt;You can check the source code for this article in &lt;a href="https://github.com/n0th1ng-else/axios-streaming-timeout" rel="noopener noreferrer"&gt;my repository&lt;/a&gt; on GitHub. The code is tested on Axios v1.5.0.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>abortsignal</category>
      <category>abortcontroller</category>
      <category>axiosjs</category>
    </item>
    <item>
      <title>Adapting One Old UI Components Library To Work In TypeScript Code</title>
      <dc:creator>Sergey Nikitin</dc:creator>
      <pubDate>Sun, 08 Aug 2021 19:48:18 +0000</pubDate>
      <link>https://dev.to/n0th1ng_else/adapting-one-old-ui-components-library-to-work-in-typescript-code-286o</link>
      <guid>https://dev.to/n0th1ng_else/adapting-one-old-ui-components-library-to-work-in-typescript-code-286o</guid>
      <description>&lt;p&gt;THE first public version of TypeScript appeared more than &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-1-0" rel="noopener noreferrer"&gt;7 years ago&lt;/a&gt;. Since that time it grew up and brought many incredible features for developers. Today it slowly becomes a standard in the JavaScript world. Slack, AirBnB, Lyft, and many others add TypeScript into their tech stack. Teams use TypeScript for both browser applications and NodeJS services. There are always pros and cons to this decision. One disadvantage is that many NPM packages are still written as JavaScript modules. We experienced this issue as well when decided to migrate our applications to TypeScript. We had to implement type definitions for our internal UI components library. We wanted to get a tool, that could serve developers as additional documentation. We also wanted to collect everything engineers can use while working with the JS library, in one place. I am going to tell you what steps did we take to achieve the desired solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type definitions
&lt;/h3&gt;

&lt;p&gt;You can describe all data that are being exported by a particular JavaScript module. The TypeScript analyzer will pick it up and will handle the package in a way you defined it in the type definitions file. The approach is close to C/C++ declaration files. Here is a simple example, imagine you have a trivial JS module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sample.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageSizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
one simple `sample.js` module might look like this





&lt;p&gt;You can use the &lt;code&gt;sample.js&lt;/code&gt; module in TypeScript code without any problems. But guess what? The analyzer would not be able to run autocomplete and infer types properly. If we want to rely on help from smart tools, we need to manually describe the API provided by our JS module. Usually, it is pretty straightforward to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sample.d.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageSizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
standard way to declare types for TypeScript is to create an appropriate `.d.ts` module





&lt;p&gt;Note that definition files have priority over JavaScript modules. Imagine you removed &lt;code&gt;export const pageSizes = [25, 50, 100]&lt;/code&gt; from the &lt;code&gt;sample.js&lt;/code&gt; module. TypeScript would still think it exists, and you will get a runtime error. It is a known tradeoff to keep definition files in sync with real JavaScript code. Teams try to update type definitions as soon as possible to provide a smooth experience for other developers. In the meantime, this approach allowed the TypeScript codebase to raise gradually without having to rewrite the whole JavaScript ecosystem.&lt;/p&gt;

&lt;p&gt;There are many examples of how to write type definitions. Most of the time you will meet simple cases and thus would be able to find something similar in the repository called &lt;a href="https://github.com/DefinitelyTyped/DefinitelyTyped" rel="noopener noreferrer"&gt;DefinitelyTyped&lt;/a&gt;, where developers store definitions for NPM packages. You can also learn more about the type definitions feature in the &lt;a href="https://www.typescriptlang.org/docs/handbook/2/type-declarations.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;. It is not a part of this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our JavaScript library
&lt;/h3&gt;

&lt;p&gt;In our company, we develop an internal UI components library. We use it in our products from the beginning, and the current production version is 12. You could only imagine how much effort it would take to rewrite such a big thing. In the meantime, we write new features using the TypeScript language. The problem is, every time one team goes to implement a new code, they write a small copy of the UI library definitions. Well, this does not sound like a good process, and we decided to have a separate package with complete type definitions for our UI components. Key points here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We would be able to import this package during the new repository initialization. This will allow controlling the version and simplify the refactoring during the version update.&lt;/li&gt;
&lt;li&gt;We would stop copy-pasting the same code again and again.&lt;/li&gt;
&lt;li&gt;Type definitions is a great documentation source. I bet developers would prefer to select the method from &lt;strong&gt;IntelliSense&lt;/strong&gt; suggestions rather than go to the web page with all API descriptions and copy the method name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  So what is wrong?
&lt;/h3&gt;

&lt;p&gt;Now you may ask me, what is wrong with our library? The thing is that we inject some global variable to interact with the exposed API. In addition, we want to import some constant pre-defined values (icons, table cell types, tag colors, etc.) that can be used by the UI components. They usually come in form of constant identifiers that help to style components.&lt;/p&gt;

&lt;p&gt;For example, we can style a button with one of the types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lists/button.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Secondary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-secondary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Danger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-danger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
depending on the value, the button will be rendered in a specific size and color palette





&lt;p&gt;We came to an idea to store all library-specific values in one place. So this project became not just type definitions for the UI library, but a real package! It should represent the exact library state at some specific version. And this is interesting - how can we implement this? Let's state what we want to achieve as the result:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We want the global variable &lt;code&gt;ui&lt;/code&gt; to be accessible without having to import anything.&lt;/li&gt;
&lt;li&gt;We want our UI components definitions to be available without having to import anything as well.&lt;/li&gt;
&lt;li&gt;We want to use predefined constants and objects for UI components by importing them from our types package. There should not be any conflict to assign some type from the library in this case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sounds like a small deal, right? Let's write some &lt;code&gt;.d.ts&lt;/code&gt; file with type definitions and... Oh, wait, you can't put real code (constants, enumerable lists, and other stuff) in the &lt;code&gt;.d.ts&lt;/code&gt; file! Sounds reasonable. Let's create a regular &lt;code&gt;.ts&lt;/code&gt; file and put all these enums there. Then we... well, how can we apply globals in the &lt;code&gt;.ts&lt;/code&gt; file?! Meh...&lt;/p&gt;

&lt;p&gt;We did not find an example of how to do that, really. StackOverflow is flooded with the &lt;code&gt;.d.ts vs .ts&lt;/code&gt; concept war. We had nothing but digging into TypeScript documentation and finally introduced the code that meets our requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start from the scratch
&lt;/h3&gt;

&lt;p&gt;First things first. We write interfaces and enums as usual. I am going to provide code examples in a simplified matter, so we would focus on the approach, not the particular code features. Imagine we have a notification dialog, so we write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// interfaces/notification.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../lists/button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Notification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
simple notification API allows assigning a text message and buttons





&lt;p&gt;Where &lt;code&gt;ButtonType&lt;/code&gt; values are from enum we saw already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lists/button.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Secondary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-secondary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Danger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-danger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we highlight a button according to the type





&lt;p&gt;Then let's take a look at the simple case. We don't import anything, as the UI components expose the global variable, and we want to call a notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// example/application/moduleNoImport.ts&lt;/span&gt;

&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Document has been saved!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we call a global API to show the notification dialog without custom button configuration





&lt;p&gt;What do we need to do to make it available? We are going to enrich the &lt;strong&gt;global&lt;/strong&gt; namespace with the &lt;code&gt;ui&lt;/code&gt; variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./interfaces/ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we simply add a new variable into the global namespace





&lt;p&gt;&lt;code&gt;UiLib&lt;/code&gt; here describes everything our UI library exposes into the global scope. In our example, we have a list of methods that show different kinds of notifications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// interfaces/ui.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Notification&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
all the notifications API is collected under the Notification interface





&lt;p&gt;This is almost it. Lastly, we adjust the package configuration. We tell TypeScript to emit type declarations by adjusting the &lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declaration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declarationDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/es"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
always emit declaration files





&lt;p&gt;We now control how TypeScript emits the output. We also specify a path to our types in &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/es/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.d.ts"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
don't forget to set up a build step for your package to compile TypeScript files





&lt;p&gt;Alright, then we install the package in our project. Finally, we specify the package path in the project's &lt;code&gt;tsconfig.json&lt;/code&gt; (since we don't use the default &lt;code&gt;@types&lt;/code&gt; folder) to see that it works!&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the values
&lt;/h3&gt;

&lt;p&gt;Now let's go deeper. What if we want to create a notification with some specific button? We want to be able to write something similar to this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// example/application/moduleWithImport.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-types-package&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sad!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Danger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we want to show notifications with the button of Danger type





&lt;p&gt;Note here and below &lt;strong&gt;UiCore&lt;/strong&gt; is a namespace that contains all the enums, configs, interfaces our UI library operates with. I think it is a good idea to collect everything under some namespace, so you would not think of names for each interface. For instance, we have a &lt;code&gt;Notification&lt;/code&gt; interface. It sounds quite abstract, and it takes a while to understand the exact object behind the naming. In the meantime &lt;code&gt;UiCore.Notification&lt;/code&gt; clearly describes where it comes from. Having a namespace is just an optional but convenient way to handle such things.&lt;/p&gt;

&lt;p&gt;Right now we can't import &lt;code&gt;UiCore&lt;/code&gt; from the library as we don't export anything. Let's improve our code and form the namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// namespaces/core.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;notificationInterfaces&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../interfaces/notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;buttonLists&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../lists/button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notificationInterfaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buttonLists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we use composite export to create an alias for objects under the namespace





&lt;p&gt;We basically export all data we have under the namespace with &lt;code&gt;export import&lt;/code&gt; alias syntax. And, since the main package module is &lt;code&gt;index.ts&lt;/code&gt; in the root, we write a global export to expose the namespace to the public:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./interfaces/ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./namespaces/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we export UiCore, and now it is available from the outside





&lt;p&gt;Two simple steps to achieve our goal! Now we can import some enum and enjoy writing the code. OR. Or we can think of some other use cases. In the example above, we used the &lt;code&gt;ButtonType.Danger&lt;/code&gt; value to create a notification with some pre-defined button. What if we want to use &lt;code&gt;ButtonType&lt;/code&gt; as a parameter type?&lt;/p&gt;

&lt;h3&gt;
  
  
  Covering edge cases
&lt;/h3&gt;

&lt;p&gt;We are not going to use some particular value, so we expect to access the type &lt;code&gt;UiCore.ButtonType&lt;/code&gt; without having to import anything. Currently, we don't have &lt;code&gt;UiCore&lt;/code&gt; in the &lt;code&gt;global&lt;/code&gt; scope and thus the code below does not work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// example/application/moduleWithType.ts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showNotificationWithButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;buttonText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;buttonType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- TS2503: Cannot find namespace 'UiCore'&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello world!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buttonText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buttonType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
TS2503: Cannot find namespace 'UiCore'





&lt;p&gt;Obviously, we are going to add the namespace in the &lt;code&gt;global&lt;/code&gt; scope. Unfortunately, we can't just use the namespace created earlier, we need to define a new one. The trick is to create a new namespace with the same name and with almost the same data included. Good news: instead of importing everything again, we can use our existing namespace to clone the data in form of types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;_UiCore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./namespaces/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./interfaces/ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_UiCore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;UiCore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_UiCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NotificationButtonConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_UiCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UiLib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
we create types from the existing namespace using the type alias syntax





&lt;p&gt;We first rename the &lt;code&gt;UiCore&lt;/code&gt; import as we want to avoid name conflict. Then we re-export &lt;code&gt;UiCore&lt;/code&gt; under the correct name as it was done previously. Finally, we copy the &lt;code&gt;UiCore&lt;/code&gt; namespace items under the global scope. Both namespaces (&lt;code&gt;UiCore&lt;/code&gt; and global &lt;code&gt;UiCore&lt;/code&gt;) export the same data. The only thing I want to draw your attention to is the way how we write export statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// UiCore under the global scope&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buttonLists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// UiCore that can be used as a value&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ButtonType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ButtonType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
different syntax for each case





&lt;p&gt;You can see the global namespace uses &lt;a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases" rel="noopener noreferrer"&gt;type alias&lt;/a&gt; syntax to define objects. For import statements, we want to have values (not types) accessible, so we can't use the same approach there. Instead, we import values and re-export them under the namespace using the composite &lt;code&gt;export import&lt;/code&gt; operator. Thus, we collect all the constants, models, enums, interfaces under some common name, we can name it whatever we want, and it will be a single entry point for all our UI library-related data. As the result, we collected all data in one place, and the developer experience does not change from using the global object to having to import something.&lt;/p&gt;

&lt;p&gt;This part is a tradeoff to get all usage cases working. It adds some copy-paste routine, but then it is a comfortable way to supply developers with type definitions: we can use the global variable exposed by the UI library as we do in JavaScript modules — without having to import anything. Then we can import the package and use constant values. All of them are defined and ready to use. The existing code will remain the same. And yes, we do support the new &lt;code&gt;import type { UiCore } from "ui-types-package"&lt;/code&gt; syntax which was introduced in TypeScript v3.8 to define types. There is no conflict with our implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;You can find thousands of existing type definitions for JavaScript libraries. In this article, I tried to explain some specific edge case, where along with type definitions, the package needs to contain real values. We use this approach for our UI components library to style table cells, specify icons, and more. You can achieve such capabilities by following these steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create and set up a new NPM package.&lt;/li&gt;
&lt;li&gt;Describe the whole interface supported by the JavaScript library you want to write type definitions for.&lt;/li&gt;
&lt;li&gt;Declare the global object that is being injected into &lt;code&gt;window&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a namespace made of objects you have defined already - you will use it for import statements.&lt;/li&gt;
&lt;li&gt;Create a namespace made of types based on the previous namespace. It will be located in the global scope.&lt;/li&gt;
&lt;li&gt;Verify that we assigned the same name for both namespaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This small guide makes it possible to cover all potential use cases for any available JS library. In the end, you will get a package, that is easy to use, support, and extend.&lt;/p&gt;

&lt;p&gt;The name &lt;code&gt;UiCore&lt;/code&gt;, the package &lt;code&gt;ui-types-package&lt;/code&gt;, and all objects in the article are placeholders to show the approach. You can use whatever names you want for your libraries and follow the idea described here.&lt;/p&gt;

&lt;p&gt;Complete code example is located &lt;a href="https://github.com/n0th1ng-else/typescript-types-complex-example" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>What I Learned From My Contribution to Angular</title>
      <dc:creator>Sergey Nikitin</dc:creator>
      <pubDate>Fri, 02 Apr 2021 16:28:37 +0000</pubDate>
      <link>https://dev.to/n0th1ng_else/what-i-learned-from-my-contribution-to-angular-h9j</link>
      <guid>https://dev.to/n0th1ng_else/what-i-learned-from-my-contribution-to-angular-h9j</guid>
      <description>&lt;p&gt;ANGULAR 9 has come, and you might notice my name in the commit history. This was the first time I contributed to such a big and widely-used project. The journey appeared to be easy enough and very exciting! Was it so? Let me tell you the story.&lt;/p&gt;

&lt;p&gt;In our company, we went through a bunch of technologies and approaches on how to develop frontend code. Initially, our service was written using the Grails framework — Java-based MVC server-side rendering engine. JQuery used to add some interactions with customers on the page which was quite common those days, but you definitely don’t want to see this in 2020.&lt;/p&gt;

&lt;p&gt;A few years later, AngularJS showed up and it was like fresh air in frontend development. Services, components, template rendering engine, etc. We were happy to implement AngularJS on our platform and wrote over 700k lines of code.&lt;/p&gt;

&lt;p&gt;Time flew and one day Angular (which was expected to be AngularJS v2) was released. The problem was that these two things are not compatible with each other, so our codebase became legacy at one moment. I pushed hard to update the version of the AngularJS and gave up on v1.5.11. What was the solution? We decided to keep existing applications as it is and to select a new way to write frontend inside the company. The thing is that our platform consists of independent applications each of them loads unrelatedly to others. Thus, every application can be written using any library or framework the team decided to use.&lt;/p&gt;

&lt;p&gt;First of all, we switched to build js code with Webpack and removed Browserify for goods. This brought us a lot of opportunities like how we split bundles, what JS features are supported, and so forth. Then the time has come and we added Typescript. After all, we implemented React on the platform. Currently, engineers develop new applications using React. But fair to say that our vision remains the same: each team decided what to use on their own. Some teams still use AngularJS because it is too difficult to re-implement existing logic. Others still fix Grails applications (yes, we still have a few of them in production at the moment!).&lt;/p&gt;

&lt;p&gt;The idea to tune up the infrastructure for Angular was flying in the air, but it was quite tricky until we started to use Webpack. With Webpack it seemed to be an easy deal: load CSS modules with raw-loader, fix Jest HTML templates loader and we good. Good enough? I thought so until we started to write an application based on the Angular framework…&lt;/p&gt;

&lt;h1&gt;
  
  
  Something Went Wrong
&lt;/h1&gt;

&lt;p&gt;The problem appeared from a place we did not expect. Let’s make some introduction: we use &lt;a href="http://ui.tradeshift.com/v12/#intro/" rel="noopener noreferrer"&gt;UI component package&lt;/a&gt; which renders a beautiful and strict UI experience for our clients. This library is framework-agnostic and acts quite similar to Material UI components for Angular i.e. developer forms a specific HTML layout to bring particular component into action, populate and style components by applying pre-defined element classes and tags. Also, one can access any element in the JS code and play with it dynamically.&lt;/p&gt;

&lt;p&gt;So it was my colleague Irina, who found an interesting issue. She was the first person to try Angular on our platform. Historically, the UI components we use rely on the HTML element attributes. These attributes have the form of &lt;strong&gt;data-ts&lt;/strong&gt; or &lt;strong&gt;data-ts.something&lt;/strong&gt;. For instance, if we want to implement &lt;a href="http://ui.tradeshift.com/v12/#components/modals/" rel="noopener noreferrer"&gt;a modal window&lt;/a&gt;, we should add the title by setting &lt;strong&gt;data-ts.title&lt;/strong&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;data-ts=&lt;/span&gt;&lt;span class="s"&gt;"Modal"&lt;/span&gt; &lt;span class="na"&gt;data-ts.title=&lt;/span&gt;&lt;span class="s"&gt;"some-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-ts=&lt;/span&gt;&lt;span class="s"&gt;"Panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Modal content.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty straightforward, right? But what if we want to apply the title dynamically? Let’s say we want the title to contain a user name or something like this. What should we do? Yes, Angular provides a standard way to interpolate the attribute value from the controller property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;data-ts=&lt;/span&gt;&lt;span class="s"&gt;"Modal"&lt;/span&gt; &lt;span class="na"&gt;[attr.data-ts.title]=&lt;/span&gt;&lt;span class="s"&gt;"modalTitle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-ts=&lt;/span&gt;&lt;span class="s"&gt;"Panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Modal content.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we go! But wait… what?! This does not look good in the browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bqtvun5mo8bau9az2vr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bqtvun5mo8bau9az2vr.png" alt="dialog element is missing data-ts.title attribute" width="799" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may notice here that the Angular compiler went through the HTML template and parsed attributes in the wrong way. For &lt;strong&gt;attr.data-ts.title&lt;/strong&gt; symbol it generates &lt;strong&gt;data-ts&lt;/strong&gt; attribute instead of &lt;strong&gt;data-ts.title&lt;/strong&gt;. This completely breaks the layout and the modal window does not work. UI components even don’t know that I defined a modal window because the attribute was overridden with the result of interpolation. It sounds like a real blocker that prevents us from using Angular.&lt;/p&gt;

&lt;h1&gt;
  
  
  Trying to Find a Solution
&lt;/h1&gt;

&lt;p&gt;I tried to google the solution but had no luck. I felt like it is meant to be a very specific case nobody noticed really. Fair enough. On the other hand, if HTML standard supports this type of element attributes and browsers render them correctly, so should Angular compiler do. Taking this into account, I decided to ask Angular team directly. I went to Angular GitHub &lt;a href="https://github.com/angular/angular" rel="noopener noreferrer"&gt;repository&lt;/a&gt; and opened an &lt;a href="https://github.com/angular/angular/issues/31334" rel="noopener noreferrer"&gt;issue&lt;/a&gt;. The beginning was promising, they marked my issue with tags that highlight that the bug exists in the Angular compiler and the issue has low priority. What happened next? I started waiting…&lt;/p&gt;

&lt;p&gt;…After 2 months nothing really happened. I figured out that since my problem is not something major, then there are low chances to see it fixed any time soon. The team is busy making the Ivy engine into a stable version. Nonetheless, they confirmed the bug exists, so they would not be against me fixing the issue by myself. Okay then, I am doing a fork of the Angular repository.&lt;/p&gt;

&lt;h1&gt;
  
  
  I am Going to Fix the Bug
&lt;/h1&gt;

&lt;p&gt;First of all, I drew my attention to the &lt;a href="https://github.com/angular/angular/blob/master/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; file and read it carefully (consider making a project fork to be my zero step). This file describes all the rules I should follow so Angular team will proceed with my Pull Request. It explains the responsibility of the parties, code ownership, agreement on the commit message format, test coverage requirements and many other questions.&lt;/p&gt;

&lt;p&gt;Next, you need to sign the Contributor License Agreement with Google company which confirms you are okay with all contributing rules and restrictions. The CLA link is located at the end of the CONTRIBUTING.md file so read the whole text until the end. Finally, paperwork comes to an end, let’s dive into the project itself.&lt;/p&gt;

&lt;p&gt;Angular is a typical yarn project, just big enough. You can simply run &lt;code&gt;yarn install&lt;/code&gt; and it will setup all the environment. Well, okay, simply run &lt;code&gt;yarn install&lt;/code&gt; and simply wait for 5–7 more minutes — I told you this is a big one!&lt;/p&gt;

&lt;p&gt;I stopped for a second at this point, because I was looking forward to an exciting journey — to go through the source code. It was scary in the beginning to figure out what is going on in such a huge project, tons of modules, different pieces of code interact with each other. But after spending some time I came to the conclusion that big projects mean to be a big advantage.&lt;/p&gt;

&lt;p&gt;When your project becomes huge, you focus on absolutely different things that might sound meaningless when you have just a few files. Different things, different approaches show up to be the priority. In the Angular, I saw that the project structure is a super important thing. Every module has a meaningful name, every variable document itself, every method shows what it does exactly. It is quite easy to navigate through the framework parts and the understanding of what this or that module is for comes to your mind instantly.&lt;/p&gt;

&lt;p&gt;I already knew that my issue lays somewhere in the Angular compiler and it was quite easy to find the &lt;a href="https://github.com/angular/angular/blob/a20979d5f613a5a2c70f26568a0e1dbf4458bf53/packages/compiler/src/template_parser/binding_parser.ts#L281" rel="noopener noreferrer"&gt;exact line&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2awgo1i3rksal73h3rz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2awgo1i3rksal73h3rz7.png" alt="Render only the first part of an attribute name? It does not sound correct to me…&lt;br&gt;
" width="799" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, what is this? We receive an HTML element attribute (stored in the &lt;strong&gt;boundProp&lt;/strong&gt;). Then we split the attribute name by some delimiter to figure out if it contains “&lt;strong&gt;attr&lt;/strong&gt;” prefix. If the first part is the prefix, we consider the attribute name to be equal to the second part. Obviously, this is not correct, we should concatenate all the parts except the prefix instead. Let’s &lt;a href="https://github.com/angular/angular/commit/c0f69f324548ed06ecdbd0b4d307f5585f620fe8#diff-b7c791684b7cf2141d91a16796befebe" rel="noopener noreferrer"&gt;fix it&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl663x3k9a7msp96vtu1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl663x3k9a7msp96vtu1k.png" alt="Now looks much better! All attribute parts are in place." width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Perfect! Now we need to make sure other changes would never break our functionality. I am going to write a test. It is quite easy to do in such huge projects like Angular. There is a requirement to cover any code change by unit tests so you definitely will find a spec file along with each module in the repository.&lt;/p&gt;

&lt;p&gt;So I opened that spec file and saw a bunch of tests that cover the module I have changed. I went through the 2000 lines of test cases and found the test which checks the name of the attribute after compiler parsed some HTML element. Made a copy and changed attribute name, so now it contains a dot delimiter and fixed the output expectation. That’s it. Now my changes are covered! I thought it would be tough, but turn out it was super easy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjea6vhsgw52vmkfau4mb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjea6vhsgw52vmkfau4mb.png" alt="All I need to cover my changes is one line test" width="798" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, I fixed the code. Writing a commit message keeping in mind the format team asked to follow and… going to the kitchen to make some tea as the pre-commit hook launches all unit tests in the project. Can you imagine having 37000+ tests in the project? The first launch will take a while, but then test results will be cached and next runs will take much less time. I made a commit, opened a &lt;a href="https://github.com/angular/angular/pull/32256" rel="noopener noreferrer"&gt;Pull Request&lt;/a&gt;, started waiting…&lt;/p&gt;

&lt;h1&gt;
  
  
  Resolution
&lt;/h1&gt;

&lt;p&gt;…After 2 months I decided waiting is enough for me. The code was the smaller problem, the bigger one was how to proceed with my changes? It was clear to me that I need to find a person who is somehow related to the Angular team and discuss what options I have right now. I applied the social engineering approach and went through the latest contributors to the Angular compiler module. &lt;a href="https://github.com/AndrewKushnir" rel="noopener noreferrer"&gt;Andrew Kushnir&lt;/a&gt; was the first on my mind. I found his Facebook account and wrote him a message. Unfortunately, a week after I did not get a follow back and decided not to be annoying for the person who doesn’t know me. I decided to have another try and found &lt;a href="https://github.com/petebacondarwin" rel="noopener noreferrer"&gt;Pete Bacon Darwin&lt;/a&gt;. I noticed that his profile has an email address so it might be useful even in 2019. And it actually did work!&lt;/p&gt;

&lt;p&gt;I wrote a long email describing all the little things we struggle with. Described our UI library, attached links on the issue and pull-request. I told about myself and my company. And most important I was really thankful as expecting Pete to spend his own time on my problem.&lt;/p&gt;

&lt;p&gt;The next day I found a message from Pete. He told me that he is okay to contact via email about my pull request and that he never saw people define attributes with dot notation. He also gave me a piece of advice on how to write an accurate commit message. And then, he approved my code changes. After this, all happened with the speed of light… while I was sleeping. (guess why? 12 hours difference between the US and Novosibirsk — the place where I live)&lt;/p&gt;

&lt;p&gt;While I was sleeping, the Angular team moved my pull request into a release state. They ran another set of tests (I guess they were integration tests this time) and figured out that in one test there was an SVG-block with some interesting attribute &lt;strong&gt;[attr.height.px]=”16"&lt;/strong&gt;. It used to be rendered as &lt;strong&gt;height=”16"&lt;/strong&gt;, but with my changes, it became &lt;strong&gt;height.px=”16"&lt;/strong&gt;. After a small discussion, they decided to fix that SVG block and to merge my changes into Angular. This meant that my fix will show up in the upcoming major version of the framework!&lt;/p&gt;

&lt;p&gt;My commit was merged, my pull request was closed. I just got up in the morning and saw all these comments, was scared, upset, and glad at the same time.&lt;/p&gt;

&lt;p&gt;In the evening I got an email from Pete. He confirmed that my fix will be released with Angular v9 and thanked me for contributing to the project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06ipz82shxzw6qnxnlm1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06ipz82shxzw6qnxnlm1.png" alt="That exact email" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From my side, I paid appreciation for the time he spent on me and for all the help. I said that it was quite an important step for me to make things right and to make a success.&lt;/p&gt;

&lt;p&gt;You may ask me what’s next?&lt;/p&gt;

&lt;p&gt;Next few months I was waiting for the release. I noticed that my changes came with Angular 9.0.0-rc.2. And now, literally, a month ago they have released the stable version of the &lt;a href="https://blog.angular.io/version-9-of-angular-now-available-project-ivy-has-arrived-23c97b63cfa3" rel="noopener noreferrer"&gt;Angular 9&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  The outcome
&lt;/h1&gt;

&lt;p&gt;For me, it was a success story. We finally got Angular based applications working on the platform. But the most important thing here is that I did my best to gain the result. So, go all the way and never give up. Try different ways to achieve what you want. Push the stuff you think is right. Look for people who can help you to find a way to deliver.&lt;/p&gt;

&lt;p&gt;If you see a big popular project in front of you, it does not mean you can’t influence it. If you have an idea or you believe something works not as expected — just try. Even if the cost of your changes is a one-line fix. In most cases, the team is open to external contributions and any help from other developers. They might have no time to fix this or that issue. So you are welcome to offer your help.&lt;/p&gt;




&lt;p&gt;I have this article posted in Russian as well, so if you want you can &lt;a href="https://habr.com/ru/post/491788/" rel="noopener noreferrer"&gt;read the translated version here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
