Typescript and the Three Perspectives of Software Development

A look at how three different personas manifest themselves in the source code and tools software engineers use.

Charlie_Chaplin

I tend to view software engineers as one of three personas: a mathematician, a machinist, or a businessperson. Tableau juggles each of these personas every day in each of our products. For example, Hyper concerns itself with correctness from a theoretical perspective (the mathematician), optimizes queries to run well on SIMD and multi-core processors (the machinist), and sits behind a PostgreSQL API so Tableau products can easily use it (the businessperson). However, these personas also manifest themselves in source code and the tools we use.

In particular, Tableau has fully embraced Typescript. Typescript is a language that, as its name suggests, adds a type system to and compiles to Javascript. It began its life as somewhat of a “classes and interfaces” object-oriented language for the businessperson but evolved to accommodate the mathematician as well. It now features tagged unions, conditional types, and generics amongst other features for any fan of type theory. As it evolved, so did our UI codebase. In doing so, we significantly improved our product quality and agility.

The mathematician

To mathematicians, software and language design should reflect the purity and exactness of math. Their tools have extremely powerful type systems, which one might be tempted to think results in much boilerplate. Quite the contrary, code written in functional languages is often terse and features little ceremony. The stereotype of a mathematician is that once they finish writing code for the first time, there’s no need to run it because the fact that it compiles at all means they’ve already arrived at the answer. Simple inspection of the 20 characters and an hour of thinking reveals its correctness. Consider the following Haskell implementation of quicksort from Learn You a Haskell for Great Good:

quicksort :: (Ord a) => [a] -> [a] quicksort [] = [] quicksort (x:xs) = let smallerSorted = quicksort [a | a <- xs, a <= x] biggerSorted = quicksort [a | a <- xs, a > x] in smallerSorted ++ [x] ++ biggerSorted

This is a fully generic implementation of quicksort that works for any types that support comparison. Strings? Yep. Doubles? Yep. Some user-defined type that implements the Ord type class? Yep. Another hint as to Haskell’s audience is given in this code:

print $ 7 ^ 401

Which happily prints:

766150423035279159796978119540663522709828411351794330434793585260807344214444463015034997928781006965179967614697777030867469971194918464102242045970078692973080467207810039328189724645053555037412152531617924129887246174839617563774787858889178415314757547059942429178035729787834413663219791782093782370738873024444042685963105185680007

No overflow. No saturation. No round off or underflow. No compromise. Just lots of numbers and a very exact value. The time and space required to arrive at this? Complexity theory is occasionally interesting, but don’t bore me with your clock cycles and bytes. They’re simply a loss of generality.

The machinist

The next persona is that of the machinist. A machinist concerns themselves very much with mechanics of how all of the moving parts work together and how they perform. Unlike the mathematician, machinists find beauty in pushing hardware and software to their design limits and doing the most with the least.

For most of my college and professional career, I’ve been in this camp. I fondly remember building a temperature sensor with a 68HC11 microcontroller, a breadboard with a few wires and an analog thermometer. In less time than it took to set up a C toolchain for our environment, I had completed the entire project using the assembler that came with the development board.

The entire program was maybe 50 bytes consisting of a simple timer interrupt that wrote values to a 16-byte ring buffer, averaged it, and wrote the average to the byte at address 0. While dereferencing a null pointer may be undefined behavior in C, address 0 is a completely fine address under our 68HC11’s configuration, so why waste it? The microcontroller only had 256 bytes of built-in memory; if you went over that budget, you’d have to add external memory and make things hard. As for the operating system that ran our program, there wasn’t. On boot, the 68HC11 in our configuration executed instructions at address 0x9000, so that’s where you found the first instruction of our program.

Machinists enjoy using low-level tools that don’t obscure the machinery in a black box. C is a machinists’ tool. While the number of moving parts in the language is simple on paper, one must be cognizant that the code will run on a real processor and has real requirements. Watch for overflow on signed integers, it’s undefined behavior in C. If you want to use the faster vector load instructions on x86 processor, your data had better be 128-bit (SSE) or 256-bit (AVX) aligned lest you get a segfault. Core 2 Duo processors trap and emulate denormalized floating point calculations in software, taking hundreds rather than a few clock cycles.

A machinist understands that our example of computing 7 to the 401st power is a futile exercise without using a big-math library. 64-bit unsigned integers overflow at 18 quintillion or so and IEEE-754 double precision integers become infinity after you slap 307 digits on them. Quad-precision floating point would be rounded, which is trade-off you may or may not be okay with. Whatever your needs are, the machinist can tell you how expensive the parts will be.

The businessperson

Finally, we arrive at the third software persona: the businessperson. The businessperson focuses their concerns with the realities of software development and chooses their tools around maximizing productivity and minimizing costs. Beautiful software to the businessperson is that which maximizes re-use, ships quickly, and doesn’t cost a ton.

When choosing a tool, the businessperson doesn’t find elegance in precision per se and mechanics are secondary to delivering value. They quickly go to market by using existing tooling and infrastructure. The business persona minimizes cost and manages complexity by using standard libraries and tools. They provide flexibility for multiple scenarios by leveraging abstraction. Sometimes to a comical fault, such as this factory method that makes factories that produce builders that emit DOM objects from an XML document in the Java standard library.

Drawing from a large talent pool and leveraging an ecosystem by choosing popular rather than “good” (as defined by the mathematician or machinist) languages is just common sense. Haskell has a reasonable ecosystem but isn’t a top-10 language on Github as of 2020. Unless it fits well into the company’s domain, it’s probably not worth the added headache of finding developers.

C, on the other hand, is a top-10 language and you’ll find many developers who know it, but it’s not ideal for most businesses outside the embedded systems and performance-critical application spaces. The bugs you can get are scary: buffer overflows, segfaults, and memory leaks come with the territory. Building and distributing C code for multiple platforms is hard (i.e., compiler toolchains, choosing a portable build system, and integrating said build system), and consuming dependencies is difficult across different operating systems.

On the other hand, Javascript, Python, and Java occupy spots 1, 2, and 3 respectively in Github’s top-10 list. Most resumes your company gets will list at least one of these languages. Each of these languages lives in an ecosystem with very prescribed recipes for building software and can consume a plethora of dependencies in a standardized format. Javascript has NPM, Python has Pip, and Java has Maven (And Gradle. And Ivy, but who’s counting?). Building and running applications in any of these languages in practice is nearly the same on Mac, Windows, and Linux.

Typescript 0.8: a language for businesspeople

I’ve used Typescript professionally since it was called Strada. In its early days, it was built for the business persona and felt like any other no-nonsense object-oriented enterprise-y language (e.g. C# and Java): classes and interfaces were the heavy movers in your type system and your app’s design felt like a C# or Java app. As such, other developers and I learned it fairly quickly.

Even in these early days, its type system provided a quantum leap in productivity (and thus, business value) over Javascript. If you mistyped a variable or function name, Visual Studio gave you a red squiggle. If you tried to pass a number to a function that took a string, compilation would fail. You didn’t have to write a bunch of logic to handle type mismatches at runtime and unit tests for said logic. You could declare a class without having to know what a prototype was.

Aside from being easy to learn and preventing simple mistakes, Typescript leveraged the existing Javascript ecosystem from its inception. If you were lazy, you could just declare ambient variables as any and throw type safety to the wind. Alternatively, you could go to Definitely Typed and incorporate a d.ts& file that gave you full typing even when the underlying library used pure Javascript. As NPM, modules, and bundlers started to become a thing, Typescript and the community adapted.

At first, the now esoteric /// <reference /> directive was the way to consume types in other files. However, Typescript quickly added explicit support for type resolution using NodeJS’s search rules, options to compile AMD and CommonJS modules, and the far more convenient import syntax. Much of Typescipt’s success results from rapidly adapting to the rest of the Javascript’s quickly changing ecosystem. Now, the ecosystem continues to adapt with Typescript; today, many NPM packages come with type definitions and for those that don’t, you can readily download a @types module.

Typescript 2.0: math goes mainstream

Compared to the old days, the zeitgeist of Typescript feels radically different: it has become a tool for the mathematician. Typescript has fully embraced the first part of its name and dived very deep on its type system. Tableau as a company has been leveraging language features almost as quickly as Typescript adds them to express invariants and push assertions into the type system. Insofar as we can, we try to prevent buggy code from compiling.

As Typescript began to cater more to the mathematician mindset, we began to embrace more type richness. This delivered significant business value in the form of increased stability and improved error handling. Examples include:

  • Dramatically increased product stability by switching on strict null checks.
  • Incorporating tagged unions to push invariants into the type system.
  • Defining state machines and validating their transitions at compile time. How we do this is at least another twoblog posts :)
  • Requiring exhaustive case handling in switch statements using the never type.

Tableau Prep’s UI codebase contains several thousand Typescript files and nearly all of them prefer types with unions and intersections over traditional deep class hierarchies and interfaces. To be sure, we do have many classes, but nearly all of them are React pure components that predated hooks.

Typescript’s machinist future?

Typescript from the start had some machinist elegance to it, as much as a language built on Javascript can. Typescript types mostly cease to exist in the compiled Javascript and thus have no overhead. Classes readily map to objects built around prototypes (pre-ES6) or just directly use the class keyword (ES6 or later). You have a very good idea in your head what the Typescript compiler will emit. Since the mapping between Typescript and compiled Javascript is straightforward, debugging is simple (assuming you don’t minify in development) even without source maps (which came in Typescript 0.8.1). However, Typescript’s future may hold much more for machinists.

In the last decade, a few efforts have had potential to unify application development between the web, server, and desktop platforms. NodeJS brought Javascript to the service world, allowing developers to use Javascript (or Typescript) alone to develop an entire web application. Electron brought web development to desktop applications: simply embed or download a web page in your branded Electron application and you now have a cross-platform desktop application. Indeed, Tableau Prep Builder is an Electron app and its Typescript code also powers Tableau Server’s flow authoring experience. These two technologies have been incursions from Javascript’s traditional web sphere of influence into desktop and server spaces. Webassembly (WASM) reverses the trend allowing traditionally native languages (e.g. Rust and C++) to target the web.

WASM came about in the last 5 years as a means to allow performance-sensitive applications to run in web browsers. However, the sandboxing is generalizable and performant enough to run without a browser. In a bizarre double reversal, server applications are beginning to use WASM as a sandboxing mechanism for server and desktop applications running outside of web browsers! In the future, we may see Typescript applications target web, server, and desktop applications and do so more efficiently than today’s Javascript runtime.

Indeed, a spinoff project called AssemblyScript has exactly this goal. It sheds some of Typescript’s Javascript trappings and instead fully buys into WASM’s virtual architecture: you can’t use the any type and number is replaced with integral and floating point types. In doing so, it exposes many of the more advanced things a machinist needs to work in the WASM world. You can force garbage collections, pin objects so the host environment or other modules can safely use them, and carte blanche allocate memory out of the GC’s purview.

Typescript and the 3 personas at Tableau

We went to great lengths to architect Tableau Prep as both a desktop and server product. The decisions we made combined tools and designs from each archetype:

  • Our machinist C++ microservice leverages AQL and Hyper to quickly get data to users so they can see their flow changes in real-time.
  • The businessperson Java REST-API deploys to online and on-premises environments interchangeably.
  • As outlined in this post, Prep’s Typescript UI incorporates mostly the businessperson and mathematician perspectives as Typescript has evolved to do so.

As Typescript, Tableau, and the software ecosystem continue to evolve, it’s possible our next product will accomplish this feat more simply using Typescript and WASM. Perhaps due to Typescript catering to more archetypes, it’s now the 4th most popular on Github and will be a compelling language for businesses for years to come. Needless to say, Tableau will continue to derive value from Typescript’s richness.