Figma’s Journey to TypeScript

https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/

Skew began as a side project in the early days of Figma. At the time, Skew fulfilled a critical need at Figma: to build out our prototype viewer with support on both the web and mobile. What started as a way to quickly spin this up became an entire compile-to-JavaScript programming language that enabled more advanced optimizations and faster compile times. But as we accumulated more and more code in Skew in the prototype viewer over the years, we slowly realized that it was difficult for new hires to ramp up on, couldn’t easily integrate with the rest of our codebase, and was missing a developer ecosystem outside of Figma. The pain of scaling it grew to outweigh its original advantages.

We recently finished migrating all Skew code at Figma to TypeScript, the industry standard language for the web. TypeScript is a sea change for the team and enables:

  • Streamlined integration with both internal and external code via static imports and native package management
  • A massive developer community that has built tools like linters, bundlers, and static analyzers
  • Modern JavaScript features like async/await and a more flexible type system
  • Seamless onboarding for new developers and lower friction for other teams

A snippet of Skew code

Left: A snippet of Skew code.

TypeScript code corresponding to this Skew code.

Right: Typescript code corresponding to this Skew code.

This migration only recently became possible for three reasons:

  • More mobile browsers started to support WebAssembly
  • We replaced many core components of our Skew engine with the corresponding components from our C++ engine, which meant we wouldn’t lose as much performance if we moved to TypeScript
  • Team growth allowed us to allocate resources to focus on the developer experience

WebAssembly saw widespread mobile support and improved performance

When we first built Figma’s mobile codebase, mobile browsers didn’t support WebAssembly and couldn’t load large bundles in a performant way. This meant that it wasn’t possible to use our main C++ engine code (which would need to compile to WebAssembly). At the same time, TypeScript was in its infancy; it wasn’t the obvious choice compared to Skew, which had static types and a stricter type system that allowed for advanced compiler optimizations. Fortunately, WebAssembly obtained widespread mobile support by 2018 and, according to our tests, reliable mobile performance by 2020.

Other performance improvements caught up to Skew’s optimizations

When we first started using Skew, there were a few key benefits: classic compiler optimizations, like constant folding and devirtualization, along with web-specific ones like generating JavaScript code with real integer operations. The longer we spent with these optimizations, the harder it was to justify a departure from a language we had cultivated for so long. For instance, in 2020, benchmarks indicated that loading Figma prototypes would’ve been nearly twice as slow using TypeScript in Safari, which was a blocker because Safari was (and still is*) the only browser engine allowed on iOS.

Some years after WebAssembly obtained widespread mobile support, we replaced many core components of our Skew engine with the corresponding components from our C++ engine. Since the components we replaced were the hottest code paths—like file loading—we wouldn’t lose as much performance if we moved to TypeScript. This experience gave us confidence that we could forego the advantages of Skew’s optimizing compiler.

Figma’s prototyping and mobile teams grew

In Figma’s earlier years, we couldn’t justify diverting resources to perform an automated migration because we were building as fast as possible with a small team. Scaling the prototyping and mobile teams into larger organizations afforded us the resources to do so.

Converting the codebase

When we first prototyped this migration in 2020, our benchmarks showed that performance would be nearly twice as slow using TypeScript. Once we saw that WebAssembly support was good enough and moved the core of the mobile engine to C++, we fixed up our old prototype during our company Maker Week. We demonstrated a working migration that passed all tests. Despite the thousands of developer experience issues and non-fatal type errors, we had a rough plan to migrate all of our Skew code safely.

Our goal was simple: convert the whole codebase to TypeScript. While we could have manually rewritten each file, we couldn’t afford to interrupt developer velocity to rewrite the entire codebase. More importantly, we wanted to avoid runtime errors and performance degradations for our users. While we ended up automating this migration, it wasn’t a quick switch. Unlike moving from another “JavaScript-with-types” language to TypeScript, Skew had actual semantic differences that made us uncomfortable with an immediate switch to TypeScript. For example, TypeScript only initializes namespaces and classes after we import a file, meaning we could run into runtime errors if we import the files in an unexpected order. By contrast, Skew makes every symbol available at runtime to the rest of the codebase upon loading, so these runtime errors wouldn’t be a problem.

Evan has said he took some learnings from this experience to make web bundler esbuild.

We opted to gradually roll out a new code bundle generated from TypeScript so that there would be minimal disruptions to developer workflows. We developed a Skew-to-TypeScript transpiler that could take Skew code as input and output generated TypeScript code, building upon the work that Evan Wallace, Figma’s former CTO, started years ago.

Phase 1: Write Skew, build Skew

We kept the original build process intact, developed the transpiler, and checked in the TypeScript code to GitHub to show developers what the new codebase would look like.

Developing a Typescript transpiler, alongside our original Skew pipeline

Developing a Typescript transpiler, alongside our original Skew pipeline

Phase 2: Write Skew, build TypeScript

Once we generated a TypeScript bundle that passed all of our unit tests, we started rolling out production traffic to build from the TypeScript codebase directly. In this phase, developers still wrote Skew, and our transpiler transpiled their code into TypeScript and updated TypeScript code living in GitHub. Additionally, we continued to fix type errors in the generated code; TypeScript could still generate a valid bundle even if there were type errors!

Rolling out production traffic to the TypeScript codebase that our TypeScript compiler generated from Skew source code

Rolling out production traffic to the TypeScript codebase that our TypeScript compiler generated from Skew source code

Phase 3: Write TypeScript, build TypeScript

Once everyone went through the TypeScript build process, we needed to make the TypeScript code the source of truth for development. After identifying a time when no one was merging code, we cut off the auto-generation process and deleted the Skew code from the codebase, effectively requiring that developers write code in TypeScript.

Making the cutover to use the Typescript codebase as the source of truth for developers

Making the cutover to use the Typescript codebase as the source of truth for developers

This was a solid approach. Having full control over the workings of the Skew compiler meant we could use it to make Phase 1 much easier; we could add and modify parts of the Skew compiler with complete freedom to satisfy our needs. Our gradual rollout also ended up paying dividends. For example, we internally caught a breakage with our Smart Animate feature as we were rolling out TypeScript. Our gated approach allowed us to quickly turn off the rollout, fix the breakage, and rethink how to proceed with our rollout plan.

We also gave ample notice about the cutover to use TypeScript. On a Friday night, we merged all the necessary changes to remove the auto-generation process and to make all of our continuous integration jobs run off the TypeScript files directly.

A note on our transpiler work

If you don’t know how compilers work, here’s a bird’s-eye view: A compiler itself consists of a frontend and a backend. The frontend is responsible for parsing and understanding the input code and performing things like type-checking and syntax checking. The frontend then converts this code to an intermediate representation (IR), a data structure that fully captures the original semantics and logic of the original input code, but structured so that we don’t need to worry about re-parsing the code.

The backend of the compiler is responsible for turning this IR into a variety of different languages. In a language like C, for example, one backend would typically generate assembly/machine code, and in the Skew compiler, the backend generates mangled and minified JavaScript.

A transpiler is a special type of compiler whose backends produce human-readable code rather than mangled machine-like code; in our case, the backend would need to take the Skew IR and produce human-readable TypeScript.

The process of writing the transpiler was relatively straightforward in the beginning: We borrowed a lot of inspiration from the JavaScript backend with the appropriate code to generate based on the information we encountered in the IR. At the tail end we ran into several issues that were trickier to track down and deal with:

  • Performance issues with array destructuring: Moving away from JavaScript array destructuring yielded up to 25% in performance benefits.
  • Skew’s “devirtualization” optimization: We took extra steps during the rollout to make sure devirtualization, a compiler optimization, did not break our codebase’s behavior.
  • Initialization order matters in TypeScript: Symbol ordering in TypeScript matters as opposed to Skew, so our transpiler needed to generate code that respected this ordering.
Performance issues with array destructuring

When investigating offline performance differences between Skew and TypeScript in some sample prototypes, we noticed that the frame rate was lower in TypeScript. After much investigation, we found out the root cause was array destructuring–which, it turns out, is rather slow in JavaScript.

To complete an operation like const [a, b] = function_that_returns_an_array(), JavaScript constructs an iterator that iterates through the array instead of directly indexing from the array, which is slower. We were doing this to retrieve arguments from JavaScript’s arguments keyword, resulting in slower performance on certain test cases. The solution was simple: We generated code to directly index the arguments array instead of destructuring, and improved per-frame latency by up to 25%!

Skew’s “devirtualization” optimization

Check out this post to learn more about devirtualization.

Another issue was divergent behavior between how TypeScript and Skew deal with class methods, which caused the aforementioned breakage in Smart Animate during our rollout. The Skew compiler does something called devirtualization, which is when–under certain conditions–a function gets pulled out of a class as a performance optimization and gets hoisted to a global function:

JavaScript

myObject.myFunc(a, b)
// becomes...
myFunc(myObject, a, b)

This optimization happens in Skew but not TypeScript. The Smart Animate breakage happened because myObject was null, and we saw different behaviors–the devirtualized call would run fine but the non-devirtualized call would result in null access exception. This made us worry if there were other such call sites that had the same problem.

To assuage our worries, we added logging in all functions that would partake in devirtualization to see if this problem had ever occurred in production. After enabling this logging for a brief period of time, we analyzed our logs and fixed all problematic call sites, making us more confident in the robustness of our TypeScript code.

Initialization order matters in TypeScript

A third issue we encountered is how each respective language honors initialization order. In Skew, you can declare variables, classes and namespaces, and function definitions anywhere in code and it won’t care about the order in which they are declared. In TypeScript, however, it does matter whether you initialize global variables or class definitions first; initializing static class variables before the class definition is a compile-time error.

Our initial version of the transpiler got around this by generating TypeScript code without using namespaces, effectively flattening every single function into the global scope. This maintained similar behavior to Skew, but the resulting code was not very readable. We reworked parts of the transpiler to emit TypeScript code in the proper order for clarity and accuracy, and added back TypeScript namespaces for readability.

Despite these challenges, we eventually built a transpiler that passed all of our unit tests and produced compiling TypeScript code that matched Skew’s performance. We opted to fix some small issues either manually in Skew source code, or once we cut over to TypeScript—rather than writing a new modification to the transpiler to fix them. While it would be ideal for all fixes to live in the transpiler, the reality is that some changes weren’t worth automating and we could move faster by fixing some issues this way.

Case study: Keeping developers happy with source maps

Throughout this process, developer productivity was always top of mind. We wanted to make the migration to TypeScript as easy as possible, which meant doing everything we could to avoid downtime and create a seamless debugging experience.

Web developers primarily debug with debuggers supplied by modern web browsers; you set a breakpoint in your source code and when the code reaches this point, the browser will pause and developers can inspect the state of the browser’s JavaScript engine. In our case, a developer would want to set breakpoints in Skew or TypeScript (depending on which phase of the project we were in).

But the browser itself can only understand JavaScript, while breakpoints are actually set in Skew or TypeScript. How does it know where to stop in the compiled JavaScript bundle given a breakpoint in source code? Enter: source maps, the way a browser knows how to link together compiled code to source code. Let’s look at a simple example with this Skew code:

Plain text

def helper() {
  return [1, 3, 4, 5];
}
def myFunc(myInt int) int {
  var arrayOfInts List<int> = helper();
  return arrayOfInts[0] + 1;
}

This code might get compiled and minified down to the following JavaScript:

JavaScript

function c(){return [1,3,4,5];}function myFunc(a){let b=c();return b[0]+1;}

This syntax is hard to read. Source maps map sections of the generated JavaScript back to specific sections of the source code (in our case, Skew). A source map between the code snippets would show mappings between:

  • helper → c
  • myInt → a
  • arrayOfInts → b

A source map normally will have file extension .map. One source map file will associate with the final JavaScript bundle so that, given a code location in the JavaScript file, the source map for our JavaScript bundle would tell us:

  • The Skew file this section of JavaScript came from
  • The code location within this Skew file that corresponds to this portion of JavaScript

Whenever a developer sets a debugger breakpoint in Skew, the browser simply reverses this source map, looks up the portion of JavaScript that this Skew line corresponds to, and sets a breakpoint there.

Here’s how we applied this to our TypeScript migration: Our original infrastructure generated Skew to JavaScript source maps that we used for debugging. However, in Phase 2 of our migration, our bundle generation pipeline was completely different, generating TypeScript followed by bundling with esbuild. If we tried to use the same source maps from our original infrastructure, we would get incorrect mappings between JavaScript and Skew code, and developers would be unable to debug their code while we’re in this phase.

We needed to generate new source maps using our new build process. This involved three pieces of work, illustrated below:

Diagram of generating new sourcemaps using our new build process

Diagram of generating new sourcemaps using our new build process

Step 1: Generate a TypeScript → JavaScript source map ts-to-js.map. esbuild can automatically generate this map when it generates the JavaScript bundle.

Step 2: Generate a Skew → TypeScript source map for each Skew source file. If we name the file file.sk, the transpiler will name the source map file.map. By emulating how the Skew → JavaScript backend of the Skew compiler creates source maps, we implemented this in our TypeScript transpiler.

Step 3: Compose these source maps together to yield a map from Skew to JavaScript. For this, we implemented the following logic in our build process:

For each entry E in ts-to-js.map:

  • Determine which TypeScript file this entry maps into and open its source map, fileX.map.
  • Look up the TypeScript code location from E in this source map, fileX.map, to obtain the code location in the corresponding Skew file fileX.sk.
  • Add this as a new entry in our final source map: the JavaScript code location from E combined with the Skew code location.

With our final source map handy, we could now map our new JavaScript bundle to Skew without disrupting the developer experience.

Case study: Conditional compilation

In Skew, top-level “if” statements allow for conditional code compilation, and we specify the conditions using compile-time constants via a “defines” option passed to the Skew compiler. We can use this to define multiple build targets—which bundle in different parts of the code—for a given codebase, so we can have different bundles for different ways of using the same codebase. For example, one bundle variant could be the actual bundle that’s deployed to users, and another could be one used only for unit testing. This allows us to specify that certain functions or classes use different implementations in debug or release builds.

To be more explicit, the following Skew code defines a different implementation for a TEST build:

Plain text

if BUILD == "TEST" {
  class HTTPRequest {
    def send(body string) HTTPResponse {
      # test-only implementation...
    }
    def testOnlyFunction {
      console.log("hi!")
    }
  }
} else {
  class HTTPRequest {
    def send(body string) HTTPResponse {
      # real implementation...
    }
  }
}

This would compile to the following JavaScript when passing a BUILD: "TEST" definition to the Skew compiler:

JavaScript

function HTTPRequest() {}
HTTPRequest.prototype.send = function(body) {
  // test-only implementation...
}
HTTPRequest.prototype.testOnlyFunction = function(body) {
  console.log("hi!")
}

However, conditional compilation is not part of TypeScript. Instead, we had to perform the conditional compilation in the build step after type-checking, as part of the bundling step using esbuild’s “defines” and dead code elimination features. The defines could therefore no longer influence type-checking, meaning code like the above example where the method testOnlyFunction is only defined in the BUILD: "TEST" build could not exist in Typescript.

We fixed this problem by converting the above Skew code to the following TypeScript code:

JavaScript

// Value defined during esbuild step
declare const BUILD: string

class HTTPRequest {   
  send(body: string): HTTPResponse {
    if (BUILD == "TEST") {
      // test-only implementation...
    } else {
      // real implementation...
    }
  }
  testOnlyFunction() {
    if (BUILD == "TEST") {
      console.log("hi!")
    } else {
      throw new Error("Unexpected call to test-only function")
    }
  }
}

This compiles to the same JavaScript code that the original Skew code also directly compiled to:

JavaScript

function HTTPRequest() {}
HTTPRequest.prototype.send = function(body) {
  // test-only implementation...
}
HTTPRequest.prototype.testOnlyFunction = function(body) {
  console.log("hi!")
}

Unfortunately, our final bundle was now slightly larger. Some symbols that were originally only available in one compile-time mode became present in all modes. For example, we only used testOnlyFunction when the build mode BUILD was set to "TEST", but after this change the function was always present in the final bundle. In our testing, we found this increase in bundle size to be acceptable. We were still be able to remove unexported top-level symbols, though, via tree-shaking.

A new era of prototyping development, now in TypeScript

By migrating all Skew code to TypeScript, we modernized a key codebase at Figma. Not only did we pave the way for it to integrate much more easily with internal and external code, developers are working more efficiently as a result. Writing the codebase initially in Skew was a good decision given the needs and capabilities of Figma at the time. However, technologies are constantly improving and we learned to never doubt the rate at which they mature. Even though TypeScript may not have been the right choice back then, it definitely is now.

We want to reap all the benefits of moving to TypeScript, so our work doesn’t stop here. We’re exploring a number of future possibilities: integration with the rest of our codebase, significantly easier package management, and direct use of new features from the active TypeScript ecosystem. We learned a lot about different facets of TypeScript—like import resolutions, module systems, and JavaScript code generation—and we can’t wait to put those learnings to good use.

We would like to thank Andrew Chan, Ben Drebing, and Eddie Shiang for their contributions to this project. If work like this appeals to you, come work with us at Figma!

{
"by": "soheilpro",
"descendants": 248,
"id": 40245686,
"kids": [
40255179,
40258098,
40255280,
40255818,
40261500,
40258165,
40255556,
40259130,
40256842,
40257392,
40256473,
40256852,
40256692,
40255804,
40259603,
40257323,
40255426,
40256818
],
"score": 261,
"time": 1714727659,
"title": "Figma’s Journey to TypeScript",
"type": "story",
"url": "https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/"
}
{
"author": null,
"date": "2024-05-01T12:00:00.000Z",
"description": "Figma’s team recently converted one of its codebases from a custom programming language to TypeScript without disrupting a single day of development.",
"image": "https://cdn.sanity.io/images/599r6htc/regionalized/bbbd715863ae9596492a6b6eda14af49a9b802de-2400x1260.png?w=1200&q=70&fit=max&auto=format",
"logo": "https://logo.clearbit.com/figma.com",
"publisher": "Figma",
"title": "Figma’s journey to TypeScript | Figma Blog",
"url": "https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/"
}
{
"url": "https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/",
"title": "Figma’s journey to TypeScript | Figma Blog",
"description": "Skew began as a side project in the early days of Figma. At the time, Skew fulfilled a critical need at Figma: to build out our prototype viewer with support on both the web and mobile. What started as a way...",
"links": [
"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/"
],
"image": "https://cdn.sanity.io/images/599r6htc/regionalized/bbbd715863ae9596492a6b6eda14af49a9b802de-2400x1260.png?w=1200&q=70&fit=max&auto=format",
"content": "<div><p><a href=\"https://evanw.github.io/skew-lang.org/\" target=\"_blank\">Skew</a> began as a side project in the early days of Figma. At the time, Skew fulfilled a critical need at Figma: to build out our prototype viewer with support on both the web and mobile. What started as a way to quickly spin this up became an entire compile-to-JavaScript programming language that enabled more advanced optimizations and faster compile times. But as we accumulated more and more code in Skew in the prototype viewer over the years, we slowly realized that it was difficult for new hires to ramp up on, couldn’t easily integrate with the rest of our codebase, and was missing a developer ecosystem outside of Figma. The pain of scaling it grew to outweigh its original advantages.</p><p>We recently finished migrating all Skew code at Figma to TypeScript, the industry standard language for the web. TypeScript is a sea change for the team and enables:</p><ul><li>Streamlined integration with both internal and external code via static imports and native package management</li><li>A massive developer community that has built tools like linters, bundlers, and static analyzers</li><li>Modern JavaScript features like <a href=\"https://javascript.info/async-await\" target=\"_blank\">async/await</a> and a more flexible type system</li><li>Seamless onboarding for new developers and lower friction for other teams</li></ul><div><figure><div><p><img alt=\"A snippet of Skew code\" src=\"https://cdn.sanity.io/images/599r6htc/regionalized/06e1934ce83f7d5429b8df3544fb46fc6961a0ae-766x406.png?w=528&amp;h=280&amp;q=75&amp;fit=max&amp;auto=format\" srcset=\"https://cdn.sanity.io/images/599r6htc/regionalized/06e1934ce83f7d5429b8df3544fb46fc6961a0ae-766x406.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.5 264w, https://cdn.sanity.io/images/599r6htc/regionalized/06e1934ce83f7d5429b8df3544fb46fc6961a0ae-766x406.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.75 396w, https://cdn.sanity.io/images/599r6htc/regionalized/06e1934ce83f7d5429b8df3544fb46fc6961a0ae-766x406.png?w=528&amp;q=75&amp;fit=max&amp;auto=format 528w, https://cdn.sanity.io/images/599r6htc/regionalized/06e1934ce83f7d5429b8df3544fb46fc6961a0ae-766x406.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=2 766w\" /></p></div><figcaption>Left: A snippet of Skew code. </figcaption></figure><figure><div><p><img alt=\"TypeScript code corresponding to this Skew code.\" src=\"https://cdn.sanity.io/images/599r6htc/regionalized/2e168248e54bbe0ae652ce2023be7018c755c17a-866x408.png?rect=1,0,865,408&amp;w=528&amp;h=249&amp;q=75&amp;fit=max&amp;auto=format\" srcset=\"https://cdn.sanity.io/images/599r6htc/regionalized/2e168248e54bbe0ae652ce2023be7018c755c17a-866x408.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.5 264w, https://cdn.sanity.io/images/599r6htc/regionalized/2e168248e54bbe0ae652ce2023be7018c755c17a-866x408.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.75 396w, https://cdn.sanity.io/images/599r6htc/regionalized/2e168248e54bbe0ae652ce2023be7018c755c17a-866x408.png?w=528&amp;q=75&amp;fit=max&amp;auto=format 528w, https://cdn.sanity.io/images/599r6htc/regionalized/2e168248e54bbe0ae652ce2023be7018c755c17a-866x408.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=1.5 792w, https://cdn.sanity.io/images/599r6htc/regionalized/2e168248e54bbe0ae652ce2023be7018c755c17a-866x408.png?w=528&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=2 865w\" /></p></div><figcaption>Right: Typescript code corresponding to this Skew code.</figcaption></figure></div><p>This migration only recently became possible for three reasons:</p><ul><li>More mobile browsers started to support WebAssembly</li><li>We replaced many core components of our Skew engine with the corresponding components from our C++ engine, which meant we wouldn’t lose as much performance if we moved to TypeScript</li><li>Team growth allowed us to allocate resources to focus on the developer experience</li></ul><h4 id=\"webassembly-saw-widespread-mobile-support-and\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#webassembly-saw-widespread-mobile-support-and\">WebAssembly saw widespread mobile support and improved performance</a></h4><p>When we first built Figma’s mobile codebase, mobile browsers didn’t support WebAssembly and couldn’t load large bundles in a performant way. This meant that it wasn’t possible to use our main C++ engine code (which would need to compile to WebAssembly). At the same time, TypeScript was in its infancy; it wasn’t the obvious choice compared to Skew, which had static types and a stricter type system <a href=\"https://github.com/evanw/esbuild/issues/771#issuecomment-775546908\" target=\"_blank\">that allowed for advanced compiler optimizations</a>. Fortunately, WebAssembly obtained widespread mobile support by 2018 and, according to our tests, reliable mobile performance by 2020.</p><h4 id=\"other-performance-improvements-caught-up-to-skew\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#other-performance-improvements-caught-up-to-skew\">Other performance improvements caught up to Skew’s optimizations</a></h4><p>When we first started using Skew, there were a few key benefits: classic compiler optimizations, like constant folding and devirtualization, along with web-specific ones like generating JavaScript code with real integer operations. The longer we spent with these optimizations, the harder it was to justify a departure from a language we had cultivated for so long. For instance, in 2020, benchmarks indicated that loading Figma prototypes would’ve been nearly twice as slow using TypeScript in Safari, which was a blocker because Safari was (and still is*) the only browser engine allowed on iOS.</p><p>Some years after WebAssembly obtained widespread mobile support, we replaced many core components of our Skew engine with the corresponding components from our C++ engine. Since the components we replaced were the hottest code paths—like file loading—we wouldn’t lose as much performance if we moved to TypeScript. This experience gave us confidence that we could forego the advantages of Skew’s optimizing compiler.</p><h4 id=\"figma-s-prototyping-and-mobile-teams-grew\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#figma-s-prototyping-and-mobile-teams-grew\">Figma’s prototyping and mobile teams grew</a></h4><p>In Figma’s earlier years, we couldn’t justify diverting resources to perform an automated migration because we were building as fast as possible with a small team. Scaling the prototyping and mobile teams into larger organizations afforded us the resources to do so.</p><h2 id=\"converting-the-codebase\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#converting-the-codebase\">Converting the codebase</a></h2><p>When we first prototyped this migration in 2020, our benchmarks showed that performance would be nearly twice as slow using TypeScript. Once we saw that WebAssembly support was good enough and moved the core of the mobile engine to C++, we fixed up our old prototype during our company <a target=\"_blank\" href=\"https://www.figma.com/blog/inside-maker-week-more-than-a-hackathon/\">Maker Week</a>. We demonstrated a working migration that passed all tests. Despite the thousands of developer experience issues and non-fatal type errors, we had a rough plan to migrate all of our Skew code safely.</p><p>Our goal was simple: convert the whole codebase to TypeScript. While we could have manually rewritten each file, we couldn’t afford to interrupt developer velocity to rewrite the entire codebase. More importantly, we wanted to avoid runtime errors and performance degradations for our users. While we ended up automating this migration, it wasn’t a quick switch. Unlike moving from another “JavaScript-with-types” language to TypeScript, Skew had actual semantic differences that made us uncomfortable with an immediate switch to TypeScript. For example, TypeScript only initializes namespaces and classes after we import a file, meaning we could run into runtime errors if we import the files in an unexpected order. By contrast, Skew makes every symbol available at runtime to the rest of the codebase upon loading, so these runtime errors wouldn’t be a problem.</p><div><p>Evan has said he took some learnings from this experience to make web bundler <a href=\"https://esbuild.github.io/\" target=\"_blank\">esbuild</a>.</p></div><p>We opted to gradually roll out a new code bundle generated from TypeScript so that there would be minimal disruptions to developer workflows. We developed a Skew-to-TypeScript transpiler that could take Skew code as input and output generated TypeScript code, building upon the work that Evan Wallace, Figma’s former CTO, started years ago.</p><h4 id=\"phase-1-write-skew-build-skew\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#phase-1-write-skew-build-skew\">Phase 1: Write Skew, build Skew</a></h4><p>We kept the original build process intact, developed the transpiler, and checked in the TypeScript code to GitHub to show developers what the new codebase would look like.</p><div><figure><div><p><img alt=\"Developing a Typescript transpiler, alongside our original Skew pipeline\" src=\"https://cdn.sanity.io/images/599r6htc/regionalized/23731112bed6116f1b3c08e5d80dc0716f93b10d-2160x1440.png?w=804&amp;h=536&amp;q=75&amp;fit=max&amp;auto=format\" srcset=\"https://cdn.sanity.io/images/599r6htc/regionalized/23731112bed6116f1b3c08e5d80dc0716f93b10d-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.5 402w, https://cdn.sanity.io/images/599r6htc/regionalized/23731112bed6116f1b3c08e5d80dc0716f93b10d-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.75 603w, https://cdn.sanity.io/images/599r6htc/regionalized/23731112bed6116f1b3c08e5d80dc0716f93b10d-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format 804w, https://cdn.sanity.io/images/599r6htc/regionalized/23731112bed6116f1b3c08e5d80dc0716f93b10d-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=1.5 1206w, https://cdn.sanity.io/images/599r6htc/regionalized/23731112bed6116f1b3c08e5d80dc0716f93b10d-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=2 1608w\" /></p></div><figcaption>Developing a Typescript transpiler, alongside our original Skew pipeline</figcaption></figure></div><h4 id=\"phase-2-write-skew-build-typescript\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#phase-2-write-skew-build-typescript\">Phase 2: Write Skew, build TypeScript</a></h4><p>Once we generated a TypeScript bundle that passed all of our unit tests, we started rolling out production traffic to build from the TypeScript codebase directly. In this phase, developers still wrote Skew, and our transpiler transpiled their code into TypeScript and updated TypeScript code living in GitHub. Additionally, we continued to fix type errors in the generated code; TypeScript could still generate a valid bundle even if there were type errors!</p><div><figure><div><p><img alt=\"Rolling out production traffic to the TypeScript codebase that our TypeScript compiler generated from Skew source code\" src=\"https://cdn.sanity.io/images/599r6htc/regionalized/f1745cf9837b710dde20a0551ac6881e496b24fd-2160x1440.png?w=804&amp;h=536&amp;q=75&amp;fit=max&amp;auto=format\" srcset=\"https://cdn.sanity.io/images/599r6htc/regionalized/f1745cf9837b710dde20a0551ac6881e496b24fd-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.5 402w, https://cdn.sanity.io/images/599r6htc/regionalized/f1745cf9837b710dde20a0551ac6881e496b24fd-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.75 603w, https://cdn.sanity.io/images/599r6htc/regionalized/f1745cf9837b710dde20a0551ac6881e496b24fd-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format 804w, https://cdn.sanity.io/images/599r6htc/regionalized/f1745cf9837b710dde20a0551ac6881e496b24fd-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=1.5 1206w, https://cdn.sanity.io/images/599r6htc/regionalized/f1745cf9837b710dde20a0551ac6881e496b24fd-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=2 1608w\" /></p></div><figcaption>Rolling out production traffic to the TypeScript codebase that our TypeScript compiler generated from Skew source code</figcaption></figure></div><h4 id=\"phase-3-write-typescript-build-typescript\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#phase-3-write-typescript-build-typescript\">Phase 3: Write TypeScript, build TypeScript</a></h4><p>Once everyone went through the TypeScript build process, we needed to make the TypeScript code the source of truth for development. After identifying a time when no one was merging code, we cut off the auto-generation process and deleted the Skew code from the codebase, effectively requiring that developers write code in TypeScript.</p><div><figure><div><p><img alt=\"Making the cutover to use the Typescript codebase as the source of truth for developers\" src=\"https://cdn.sanity.io/images/599r6htc/regionalized/2721f0f0937fa5609e35d23b4cab6c47237cf3e6-2160x1440.png?w=804&amp;h=536&amp;q=75&amp;fit=max&amp;auto=format\" srcset=\"https://cdn.sanity.io/images/599r6htc/regionalized/2721f0f0937fa5609e35d23b4cab6c47237cf3e6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.5 402w, https://cdn.sanity.io/images/599r6htc/regionalized/2721f0f0937fa5609e35d23b4cab6c47237cf3e6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.75 603w, https://cdn.sanity.io/images/599r6htc/regionalized/2721f0f0937fa5609e35d23b4cab6c47237cf3e6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format 804w, https://cdn.sanity.io/images/599r6htc/regionalized/2721f0f0937fa5609e35d23b4cab6c47237cf3e6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=1.5 1206w, https://cdn.sanity.io/images/599r6htc/regionalized/2721f0f0937fa5609e35d23b4cab6c47237cf3e6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=2 1608w\" /></p></div><figcaption>Making the cutover to use the Typescript codebase as the source of truth for developers</figcaption></figure></div><p>This was a solid approach. Having full control over the workings of the Skew compiler meant we could use it to make Phase 1 much easier; we could add and modify parts of the Skew compiler with complete freedom to satisfy our needs. Our gradual rollout also ended up paying dividends. For example, we internally caught a breakage with our Smart Animate feature as we were rolling out TypeScript. Our gated approach allowed us to quickly turn off the rollout, fix the breakage, and rethink how to proceed with our rollout plan.</p><p>We also gave ample notice about the cutover to use TypeScript. On a Friday night, we merged all the necessary changes to remove the auto-generation process and to make all of our continuous integration jobs run off the TypeScript files directly.</p><h2 id=\"a-note-on-our-transpiler-work\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#a-note-on-our-transpiler-work\">A note on our transpiler work</a></h2><p>If you don’t know how compilers work, here’s a bird’s-eye view: A compiler itself consists of a frontend and a backend. The frontend is responsible for parsing and understanding the input code and performing things like type-checking and syntax checking. The frontend then converts this code to an<strong> </strong>intermediate representation (IR), a data structure that fully captures the original semantics and logic of the original input code, but structured so that we don’t need to worry about re-parsing the code.</p><p>The backend of the compiler is responsible for turning this IR into a variety of different languages. In a language like C, for example, one backend would typically generate assembly/machine code, and in the Skew compiler, the backend generates mangled and minified JavaScript.</p><div><p>A <strong>transpiler</strong> is a special type of compiler whose backends produce human-readable code rather than mangled machine-like code; in our case, the backend would need to take the Skew IR and produce human-readable TypeScript.</p></div><p>The process of writing the <strong>transpiler</strong> was relatively straightforward in the beginning: We borrowed a lot of inspiration from the JavaScript backend with the appropriate code to generate based on the information we encountered in the IR. At the tail end we ran into several issues that were trickier to track down and deal with:</p><ul><li><strong>Performance issues with array destructuring:</strong> Moving away from JavaScript array destructuring yielded up to 25% in performance benefits.</li><li><strong>Skew’s “devirtualization” optimization: </strong>We took extra steps during the rollout to make sure devirtualization, a compiler optimization, did not break our codebase’s behavior.</li><li><strong>Initialization order matters in TypeScript:</strong> Symbol ordering in TypeScript matters as opposed to Skew, so our transpiler needed to generate code that respected this ordering.</li></ul><h5 id=\"performance-issues-with-array-destructuring\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#performance-issues-with-array-destructuring\">Performance issues with array destructuring</a></h5><p>When investigating offline performance differences between Skew and TypeScript in some sample prototypes, we noticed that the frame rate was lower in TypeScript. After much investigation, we found out the root cause was array destructuring–which, it turns out, is rather slow in JavaScript.</p><p>To complete an operation like <code>const [a, b] = function_that_returns_an_array()</code>, JavaScript constructs an iterator that iterates through the array instead of directly indexing from the array, which is slower. We were doing this to retrieve arguments from JavaScript’s <code>arguments</code> keyword, resulting in slower performance on certain test cases. The solution was simple: We generated code to directly index the arguments array instead of destructuring, and improved per-frame latency by up to 25%!</p><h5 id=\"skew-s-devirtualization-optimization\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#skew-s-devirtualization-optimization\">Skew’s “devirtualization” optimization</a></h5><div><p>Check out <a href=\"https://marcofoco.com/blog/2016/10/03/the-power-of-devirtualization/\" target=\"_blank\">this post</a> to learn more about devirtualization.</p></div><p>Another issue was divergent behavior between how TypeScript and Skew deal with class methods, which caused the aforementioned breakage in Smart Animate during our rollout. The Skew compiler does something called <strong>devirtualization</strong>, which is when–under certain conditions–a function gets pulled out of a class as a performance optimization and gets hoisted to a global function:</p><div><p><span>JavaScript</span></p><div><pre><code><span>myObject.myFunc(a, b)\n</span><span></span><span>// becomes...</span><span>\n</span>myFunc(myObject, a, b)</code></pre></div></div><p>This optimization happens in Skew but not TypeScript. The Smart Animate breakage happened because <code>myObject</code> was null, and we saw different behaviors–the devirtualized call would run fine but the non-devirtualized call would result in null access exception. This made us worry if there were other such call sites that had the same problem.</p><p>To assuage our worries, we added logging in all functions that would partake in devirtualization to see if this problem had ever occurred in production. After enabling this logging for a brief period of time, we analyzed our logs and fixed all problematic call sites, making us more confident in the robustness of our TypeScript code.</p><h5 id=\"initialization-order-matters-in-typescript\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#initialization-order-matters-in-typescript\">Initialization order matters in TypeScript</a></h5><p>A third issue we encountered is how each respective language honors initialization order. In Skew, you can declare variables, classes and namespaces, and function definitions anywhere in code and it won’t care about the order in which they are declared. In TypeScript, however, it <em>does</em> matter whether you initialize global variables or class definitions first; initializing static class variables before the class definition is a compile-time error.</p><p>Our initial version of the transpiler got around this by generating TypeScript code without using namespaces, effectively flattening every single function into the global scope. This maintained similar behavior to Skew, but the resulting code was not very readable. We reworked parts of the transpiler to emit TypeScript code in the proper order for clarity and accuracy, and added back TypeScript namespaces for readability.</p><p>Despite these challenges, we eventually built a transpiler that passed all of our unit tests and produced compiling TypeScript code that matched Skew’s performance. We opted to fix some small issues either manually in Skew source code, or once we cut over to TypeScript—rather than writing a new modification to the transpiler to fix them. While it would be ideal for all fixes to live in the transpiler, the reality is that some changes weren’t worth automating and we could move faster by fixing some issues this way.</p><h2 id=\"case-study-keeping-developers-happy-with-source\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#case-study-keeping-developers-happy-with-source\">Case study: Keeping developers happy with source maps</a></h2><p>Throughout this process, developer productivity was always top of mind. We wanted to make the migration to TypeScript as easy as possible, which meant doing everything we could to avoid downtime and create a seamless debugging experience.</p><p>Web developers primarily debug with debuggers supplied by modern web browsers; you set a <em>breakpoint</em> in your source code and when the code reaches this point, the browser will pause and developers can inspect the state of the browser’s JavaScript engine. In our case, a developer would want to set breakpoints in Skew or TypeScript (depending on which phase of the project we were in).</p><p>But the browser itself can only understand JavaScript, while breakpoints are actually set in Skew or TypeScript. How does it know where to stop in the compiled JavaScript bundle given a breakpoint in source code? Enter: source maps, the way a browser knows how to link together compiled code to source code. Let’s look at a simple example with this Skew code:</p><div><p><span>Plain text</span></p><div><pre>def helper() {\n return [1, 3, 4, 5];\n}\ndef myFunc(myInt int) int {\n var arrayOfInts List&lt;int&gt; = helper();\n return arrayOfInts[0] + 1;\n}</pre></div></div><p>This code might get compiled and minified down to the following JavaScript:</p><div><p><span>JavaScript</span></p><div><pre><code><span>function</span><span> </span><span>c</span><span>(</span><span>)</span><span>{</span><span>return</span><span> [</span><span>1</span><span>,</span><span>3</span><span>,</span><span>4</span><span>,</span><span>5</span><span>];}</span><span>function</span><span> </span><span>myFunc</span><span>(</span><span>a</span><span>)</span><span>{</span><span>let</span><span> b=c();</span><span>return</span><span> b[</span><span>0</span><span>]+</span><span>1</span><span>;}</span></code></pre></div></div><p>This syntax is hard to read. Source maps map sections of the generated JavaScript back to specific sections of the source code (in our case, Skew). A source map between the code snippets would show mappings between:</p><ul><li><code>helper → c</code></li><li><code>myInt → a</code></li><li><code>arrayOfInts → b</code></li></ul><p>A source map normally will have file extension <code>.map</code>. One source map file will associate with the final JavaScript bundle so that, given a code location in the JavaScript file, the source map for our JavaScript bundle would tell us:</p><ul><li>The Skew file this section of JavaScript came from</li><li>The code location within this Skew file that corresponds to this portion of JavaScript</li></ul><p>Whenever a developer sets a debugger breakpoint in Skew, the browser simply reverses this source map, looks up the portion of JavaScript that this Skew line corresponds to, and sets a breakpoint there.</p><p>Here’s how we applied this to our TypeScript migration: Our original infrastructure generated Skew to JavaScript source maps that we used for debugging. However, in Phase 2 of our migration, our bundle generation pipeline was completely different, generating TypeScript followed by bundling with esbuild. If we tried to use the same source maps from our original infrastructure, we would get incorrect mappings between JavaScript and Skew code, and developers would be unable to debug their code while we’re in this phase.</p><p>We needed to generate new source maps using our new build process. This involved three pieces of work, illustrated below:</p><div><figure><div><p><img alt=\"Diagram of generating new sourcemaps using our new build process\" src=\"https://cdn.sanity.io/images/599r6htc/regionalized/286fc5900a860382eb604f2b2831e567cfd74ca6-2160x1440.png?w=804&amp;h=536&amp;q=75&amp;fit=max&amp;auto=format\" srcset=\"https://cdn.sanity.io/images/599r6htc/regionalized/286fc5900a860382eb604f2b2831e567cfd74ca6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.5 402w, https://cdn.sanity.io/images/599r6htc/regionalized/286fc5900a860382eb604f2b2831e567cfd74ca6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=0.75 603w, https://cdn.sanity.io/images/599r6htc/regionalized/286fc5900a860382eb604f2b2831e567cfd74ca6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format 804w, https://cdn.sanity.io/images/599r6htc/regionalized/286fc5900a860382eb604f2b2831e567cfd74ca6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=1.5 1206w, https://cdn.sanity.io/images/599r6htc/regionalized/286fc5900a860382eb604f2b2831e567cfd74ca6-2160x1440.png?w=804&amp;q=75&amp;fit=max&amp;auto=format&amp;dpr=2 1608w\" /></p></div><figcaption>Diagram of generating new sourcemaps using our new build process</figcaption></figure></div><p><strong>Step 1</strong>: Generate a TypeScript → JavaScript source map <code>ts-to-js.map</code>. esbuild can automatically generate this map when it generates the JavaScript bundle.</p><p><strong>Step 2</strong>: Generate a Skew → TypeScript source map for each Skew source file. If we name the file <code>file.sk</code>, the transpiler will name the source map <code>file.map</code>. By emulating how the Skew → JavaScript backend of the Skew compiler creates source maps, we implemented this in our TypeScript transpiler.</p><p><strong>Step 3</strong>: Compose these source maps together to yield a map from Skew to JavaScript. For this, we implemented the following logic in our build process:</p><p>For each entry <code>E</code> in <code>ts-to-js.map</code>:</p><ul><li>Determine which TypeScript file this entry maps into and open its source map, <code>fileX.map</code>.</li><li>Look up the TypeScript code location from <code>E</code> in this source map, <code>fileX.map</code>, to obtain the code location in the corresponding Skew file <code>fileX.sk</code>.</li><li>Add this as a new entry in our final source map: the JavaScript code location from <code>E</code> combined with the Skew code location.</li></ul><p>With our final source map handy, we could now map our new JavaScript bundle to Skew without disrupting the developer experience.</p><h2 id=\"case-study-conditional-compilation\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#case-study-conditional-compilation\">Case study: Conditional compilation</a></h2><p>In Skew, top-level “if” statements allow for conditional code compilation, and we specify the conditions using compile-time constants via a “defines” option passed to the Skew compiler. We can use this to define multiple build targets—which bundle in different parts of the code—for a given codebase, so we can have different bundles for different ways of using the same codebase. For example, one bundle variant could be the actual bundle that’s deployed to users, and another could be one used only for unit testing. This allows us to specify that certain functions or classes use different implementations in debug or release builds.</p><p>To be more explicit, the following Skew code defines a different implementation for a <code>TEST</code> build:</p><div><p><span>Plain text</span></p><div><pre>if BUILD == \"TEST\" {\n class HTTPRequest {\n def send(body string) HTTPResponse {\n # test-only implementation...\n }\n def testOnlyFunction {\n console.log(\"hi!\")\n }\n }\n} else {\n class HTTPRequest {\n def send(body string) HTTPResponse {\n # real implementation...\n }\n }\n}</pre></div></div><p>This would compile to the following JavaScript when passing a <code>BUILD: \"TEST\"</code> definition to the Skew compiler:</p><div><p><span>JavaScript</span></p><div><pre><code><span>function</span><span> </span><span>HTTPRequest</span><span>(</span><span>) </span><span>{}\n</span><span>HTTPRequest.prototype.send = </span><span>function</span><span>(</span><span>body</span><span>) </span><span>{\n</span><span> </span><span>// test-only implementation...</span><span>\n</span>}\n<span>HTTPRequest.prototype.testOnlyFunction = </span><span>function</span><span>(</span><span>body</span><span>) </span><span>{\n</span><span> </span><span>console</span><span>.log(</span><span>\"hi!\"</span><span>)\n</span>}</code></pre></div></div><p>However, conditional compilation is not part of TypeScript. Instead, we had to perform the conditional compilation in the build step <em>after</em> type-checking, as part of the bundling step using esbuild’s “defines” and dead code elimination features. The defines could therefore no longer influence type-checking, meaning code like the above example where the method <code>testOnlyFunction</code> is only defined in the <code>BUILD: \"TEST\"</code> build could not exist in Typescript.</p><p>We fixed this problem by converting the above Skew code to the following TypeScript code:</p><div><p><span>JavaScript</span></p><div><pre><code><span>// Value defined during esbuild step</span><span>\n</span><span>declare </span><span>const</span><span> BUILD: string\n</span>\n<span></span><span>class</span><span> </span><span>HTTPRequest</span><span> </span><span>{ \n</span> send(body: string): HTTPResponse {\n<span> </span><span>if</span><span> (BUILD == </span><span>\"TEST\"</span><span>) {\n</span><span> </span><span>// test-only implementation...</span><span>\n</span><span> } </span><span>else</span><span> {\n</span><span> </span><span>// real implementation...</span><span>\n</span> }\n }\n<span> </span><span>testOnlyFunction</span><span>(</span><span>)</span><span> {\n</span><span> </span><span>if</span><span> (BUILD == </span><span>\"TEST\"</span><span>) {\n</span><span> </span><span>console</span><span>.log(</span><span>\"hi!\"</span><span>)\n</span><span> } </span><span>else</span><span> {\n</span><span> </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>\"Unexpected call to test-only function\"</span><span>)\n</span> }\n }\n}</code></pre></div></div><p>This compiles to the same JavaScript code that the original Skew code also directly compiled to:</p><div><p><span>JavaScript</span></p><div><pre><code><span>function</span><span> </span><span>HTTPRequest</span><span>(</span><span>) </span><span>{}\n</span><span>HTTPRequest.prototype.send = </span><span>function</span><span>(</span><span>body</span><span>) </span><span>{\n</span><span> </span><span>// test-only implementation...</span><span>\n</span>}\n<span>HTTPRequest.prototype.testOnlyFunction = </span><span>function</span><span>(</span><span>body</span><span>) </span><span>{\n</span><span> </span><span>console</span><span>.log(</span><span>\"hi!\"</span><span>)\n</span>}</code></pre></div></div><p>Unfortunately, our final bundle was now slightly larger. Some symbols that were originally only available in one compile-time mode became present in all modes. For example, we only used <code>testOnlyFunction</code> when the build mode <code>BUILD</code> was set to <code>\"TEST\"</code>, but after this change the function was always present in the final bundle. In our testing, we found this increase in bundle size to be acceptable. We were still be able to remove unexported top-level symbols, though, via <a href=\"https://en.wikipedia.org/wiki/Tree_shaking\" target=\"_blank\">tree-shaking</a>.</p><h2 id=\"a-new-era-of-prototyping-development-now-in\"><a target=\"_blank\" href=\"https://www.figma.com/blog/figmas-journey-to-typescript-compiling-away-our-custom-programming-language/#a-new-era-of-prototyping-development-now-in\">A new era of prototyping development, now in TypeScript</a></h2><p>By migrating all Skew code to TypeScript, we modernized a key codebase at Figma. Not only did we pave the way for it to integrate much more easily with internal and external code, developers are working more efficiently as a result. Writing the codebase initially in Skew was a good decision given the needs and capabilities of Figma at the time. However, technologies are constantly improving and we learned to never doubt the rate at which they mature. Even though TypeScript may not have been the right choice back then, it definitely is now.</p><p>We want to reap all the benefits of moving to TypeScript, so our work doesn’t stop here. We’re exploring a number of future possibilities: integration with the rest of our codebase, significantly easier package management, and direct use of new features from the active TypeScript ecosystem. We learned a lot about different facets of TypeScript—like import resolutions, module systems, and JavaScript code generation—and we can’t wait to put those learnings to good use.</p><svg width=\"93\" height=\"13\"></svg><p><em>We would like to thank Andrew Chan, Ben Drebing, and Eddie Shiang for their contributions to this project. If work like this appeals to you, come <a target=\"_blank\" href=\"https://www.figma.com/careers/#job-openings\">work with us at Figma</a>!</em></p></div>",
"author": "",
"favicon": "https://static.figma.com/app/icon/1/favicon.svg",
"source": "figma.com",
"published": "",
"ttr": 648,
"type": "website"
}