Support and adoption of TypeScript has grown exponentially in the last few years. Companies and products like Netflix, Airbnb, Slack, and Lyft, just to name a few, all utilize TypeScript in their applications.
It offers a great developer experience, and can dramatically increase an engineering team’s productivity by catching errors earlier. Sparkbox considers it our development standard, and we lean on TypeScript for every project when it makes sense and is appropriate for the client’s needs. Why is that, you may ask?
TL;DR: TypeScript wants to squash bugs before they bother your users
Yes, TypeScript’s ability to catch most code bugs and automatically check types are arguably the biggest benefits to using TypeScript. Type checking can lead to fewer bugs, which can, in turn, increase the quality of an application and ensure a reliable user experience. It also helps save developers’ time by identifying issues sooner in the development process and preventing costly QA cycles. How does TypeScript do it? To understand the answer to that question, we need a little bit of history and a simple example.
TypeScript (TS) is what they call a “superset” of JavaScript (JS) - an extension of JS that incorporates additional syntax and features. All JS is valid TS, but the added TS features (most notably the type annotations) provide clarification to your code, almost like metadata, describing how your code should be used, and adding protections to ensure it’s used correctly.
JS is what we call dynamically typed, so it can be a little flexible with how it accepts data, right? TS is (optionally) strictly typed, which means it’s not nearly as flexible. This may sound like a drawback at first, but it requires us to consider the types of data we use, like string
, number
, boolean
, null
, and undefined
, early on in development. Ultimately, TS transpiles down to plain JS, but it provides crucial type checking in the process. Let’s look at an example.
// The intent of this function, written in plain JS, is to add a couple of numbers together to get the sum (keep this intention in mind).
const addTheseNumbersJS = (a, b) => {
return a + b;
}
// In this case, we're passing a string, and a number.
addTheseNumbersJS("4", 5); // output: "45"
If, for some reason, I accidentally passed this function a string
type, like “4″
above, this code would still run. JS would concatenate those variables instead of adding them together like I meant, but the code would still successfully execute at runtime. Unfortunately however, my tests would fail, and I’d have to track down that bug with the string
type. Without TypeScript, developers have to run extra checks to ensure that the function only accepts number
types.
Let’s use the same accidental string
passed to our function, but this time, add manual type checking. This approach is totally possible and valid:
const addTheseNumbersJS = (a, b) => {
// This `if-else` block checks that both arguments are of the type `number` first before adding them, and throws an error when they aren't.
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else {
throw new Error("Uh oh- please use numbers only!");
}
}
addTheseNumbersJS("4", 5); // throws error: "Uh oh- please use numbers only!"
This way of type checking can be time consuming, arduous (especially as a codebase becomes more complex), and sometimes easy to forget, but again, it’s totally valid.
Enter TS. The data flowing through this function can be declared as a certain type up front, and even the function’s return can be typed.
const addTheseNumbersTS = (a: number, b: number): number => {
return a + b;
};
addTheseNumbersTS("4", 5); // TS throws an error here to warn the developer that a string has been passed to addTheseNumbersTS instead of a number.
addTheseNumbersTS(4, 5)); // output: 45
Now with our function written in TS with type annotations, if this function receives incorrect data, we get errors and suggestions on how to resolve them.
With the type safety of TS, that accidental string
won’t even flow through this function, because we’ve specified number
types only. Once that parameter’s type is corrected, the function adds the two variables as expected, and any tests surrounding this function pass.
To be fair, you can absolutely add types to vanilla JS using a JSDoc comment. This approach is well adopted, and TS even has a section dedicated to JSDoc in their handbook since it’s a valid way to add a type system to JS projects.
In the example below, we have declared that both a
and b
will be of the type number
, and the addTheseNumbersJSDoc
function will return a number
.
// We can declare the parameters and return types in a doc block signified by `/**` right above the code we are typing.
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function addTheseNumbersJSDoc(a, b) {
return a + b;
}
addTheseNumbersJSDoc("4", 5); // incorrect output: "45" (string)
addTheseNumbersJSDoc(4, 5); // correct output: 9 (number)
Unfortunately, as seen in the above example, we don’t get the built-in type checking with JSDoc like we get in TS. But we can provide hints to the developer if there are type errors by adding @ts-check
at the top of a JS file. With @ts-check
enabled, we’ll see the same dreaded red squiggles and editor warnings when types are passed incorrectly. This still doesn’t provide any enforcement at build time, however, whereas with TS, a build error would be thrown.
Hovering over the variables in a code editor will also reveal their types, which can help when debugging.
Using JSDoc like this can sometimes feel noisy when there are type definitions in every file, and types may end up declared multiple times over multiple files. Instead, we like to organize our type definitions into separate files. This separated approach is particularly useful for shared types, so we can increase the reusability of certain types. Rather than declaring types multiple times with JSDoc, they are extracted into a common type, and can be reused across add
, subtract
, and multiply
functions to create even more specific types. TS makes it easy and tidy to organize all of your type definitions in one place.
By making a development team explicitly state the types of data an app should be consuming and using, TS is ensuring that less bugs are introduced and the code functions properly more often. It makes for more resilient, maintainable code.
TypeScript Benefits
If you search for testimonials, one can see why TS got adopted so quickly, by so many:
“I get a little bit more confidence in what I’m releasing, because that type checking is going to make sure your code is actually doing [what it was defined to do] or it’s in the structure that it’s defined. And…I guess that structure that you define, also gives you a level of documentation, so you know exactly what this function, method or whatever is taking, and what that’s going to bring back.” -Joe King, Front End Happy Hour Podcast
“It’s able to identify those [errors] instantly as you’re writing the code rather than that error ever making it to the users or even having to have that cognitive load of having to remember to test stuff like [types].” -Wes Bos, Syntax Podcast
Here’s some of the more specific benefits we find in using TS in our projects.
Error & Edge Cases
TS doesn’t run in the browser. It transpiles code to pure JS first, which then runs in the browser. When there are type errors, TS won’t transpile code to JS, which means that the odds are really good that errors won’t even make it to users.
When it comes to physically writing code, TS integrates very tightly with editors like VSCode. That integration allows for easy code validation. For instance, TS will check for syntax errors, like misspellings, and offer suggestions to correct those. Additionally, TS will alert you if a property doesn’t exist on the type you’re trying to use, or give you an error if you’re missing properties. It doesn’t catch all bugs, but it sure catches lots. All of these innocent bugs are surfaced by TS while a developer is writing code and get squashed earlier on in the process, instead of revealing themselves during runtime.
To improve scalability even more, we at Sparkbox tend to configure TS with the strict settings. It enforces good habits by reminding us to handle errors, and can help shine a light on blind spots in our code. It requires us to be more verbose with code and account for lots of edge cases.
Support for JSX and React
Many apps are written in React and JSX. JSX, which is a “syntax extension” to JS, is meant to compile directly to JS, just like TypeScript. There’s a whole, dedicated types package for teams looking to get the support and benefits of TS in their React projects. Once @types/react
is installed in a TypeScript codebase, developers don’t have to do too much extra type coercion or custom wizardry. The typing is quite extensive, so most things will “just work.” TS will compile any JSX into plain JS, if you choose. All of this means we can still use JSX, while also getting the security of more robust type checking.
Typing in React projects can be really excellent for validating props (element attribute types
) as they get passed into components, and is especially good for custom props like component children
. Another good scenario for type checking would be React state
, which can be typed with custom types, or inferred by TS if your state
is one of the built-in primitive data types. If TS is inferring types, the function that updates your state (the one that gets returned with the useState
hook) can be automatically typed as well, and you don’t have to do anything else! The side effects of the useEffect
hook can be typed without doing anything special, too! Even React events can be typed, and TS can infer the type of event, like a mouse click, based on the element that the event is attached to. In a code editor, writing React.
(“React dot”) will trigger TypeScript React to suggest event types to you that correspond with DOM events. This means less custom event work, as well as access to the e.target
methods (and more!).
Type Safety
TS can be configured to be strict. TypeScript has a few concepts, like type safety, type guards, and type narrowing, that have your best coding interests in mind. All try to provide assurance that the correct types of data are flowing through your application properly, that the data looks like the data you’re expecting (think of the type number 99
vs. the type string “99”
). Although it can look cluttered sometimes, using TS gives some semblance of built-in instructions on what type of data your functions can take in, and what type of data they return. That type safety allows our entire team to understand how we write code in any particular project.
Type Inference
In TypeScript, we have implicit and explicit types. We can either declare them in the code, or let TS infer the types for us. Type inference is really powerful. You don’t actually have to attach types to every piece of code that you write. TS can recognize when variables don’t actually need to be explicitly typed, based on the type of value assigned to those variables. Implicit types are good for local variables - let things be inferred if and when possible. Explicit typing has a place for public or exported code, and function inputs and outputs.
External Library Typing
Some libraries ship from npm
with TS types. From there, you can import the library types for functions, inputs, outputs, everything. Some libraries, however, aren’t written in TS or haven’t been converted yet. Fear not, for there is a movement and a community dedicated to rewriting them called Definitely Typed. You may have seen packages before with the @types
prefix, like @types/react
, and those are the packages Definitely Typed maintains to support TS usage.
Ease of Refactoring
TS knows exactly where all variables, inputs and outputs in your entire app are. Even if there’s code that is just repetitive, code editors can leverage TS to help refactor that code. Code editors that are tightly integrated with TS can utilize TS’s “knowledge” of the codebase to extract functions to their own modules, move functions to new files and automatically refactor imports, and any renamed variables or functions. That sort of development assistance gives us much more confidence than a “find and replace.”
If you haven’t yet refactored and migrated from JS to TS, don’t fret. It’s actually pretty easy, and can be done in small chunks. Don’t believe us? Take it from TypeScript themselves:
…if you move code from JavaScript to TypeScript, it is guaranteed to run the same way, even if TypeScript thinks that the code has type errors.
Keeping the same runtime behavior as JavaScript is a foundational promise of TypeScript because it means you can easily transition between the two languages without worrying about subtle differences that might make your program stop working.
Even More Bonus Benefits
TS has really excellent community support. The TS documentation is highly developed, quite expansive and covers almost everything you might need, even as a TypeScript newbie.
By using the typing system, you also get some documentation right in the code. That shouldn’t prevent you from actually curating documentation for a codebase, though!
There’s several different tools for type checking and compiling your code to JS. TypeScript comes with
tsc
out of the box to do those tasks, but it can be a slow part of the feedback loop while it validates your code. Some speedier solutions for compiling your TS could be to use SWC or esbuild, but neither do type checking. Tools like Vite (which uses esbuild) and Parcel (which uses SWC) do type checking and compilation.On the server side, using TS with Node, Deno, or Bun can help your app run faster.
Other integrations with Prettier, Next.js, Jest, and Gatsby are all pretty tightened up.
When is TypeScript Worth It?
To Sparkbox, TS reinforces good coding practices that permeate our entire team, regardless of their technical skill level or knowledge. It gets everybody on the same page, thinking about the shape of our data in a consistent way. We also believe that if a project is going to “live” longer, has many contributors, or is a large codebase, we want the scalability and robustness of TS. It offers a maintainable codebase for long-term or large-scale projects.
Resources
Tasty TypeScript, with Wes Bos
Syntax Podcast, TypeScript Fundamentals
Syntax Podcast, React & TypeScript