NEW

Prismic offers an ideal solution to feature your e-commerce products in your promotional landing pages or inspirational content. View more

When to Use TypesScript: Pros and Cons for JavaScript Devs

Written by Alexander Dubovoy in
concept
on June 15, 2022

It feels like just about every article these days encourages us to use TypeScript. While TypeScript may have seemed to some JavaScript developers as a niche concept at the beginning, it has undoubtedly grown in popularity and influence. So, you may be wondering if it’s worth joining the TypeScript hype train 🚂

The good news is that you don’t have to learn millions of new concepts to test TypeScript out and see if it works for you. The vast majority of TypeScript looks just like JavaScript, so you’ll find most of it understandable if you’re already familiar with JavaScript.

In this article, I’ll compare how TypeScript would change existing JavaScript codebases in order to understand what additional syntax you might have to learn to understand TypeScript. I’ll try to maintain a broader focus on key syntax concepts, rather than tying my explanation to a single framework or type of code.

Through this process, I’ll explain TypeScript’s benefits and when I think it’s worth adding to a project. In my experience, TypeScript is particularly useful for larger projects where identifying a small “developer error” (like a typo or an edge case) can become time-consuming and painful.

I’ll also address TypeScript’s cons. For all its hype, TypeScript is less a fast/new tool and, instead, makes writing JavaScript slower and more methodical, which can help regularize code and prevent common errors. On the flip side, however, TypeScript simply isn’t worth it on some smaller projects, or if the project isn’t well suited to static typing. And, it may not be worth adding TypeScript to a project if you want to include more junior developers who may have a harder time picking it up.

Regardless, I think that familiarity with TypeScript can help us write better JavaScript and that there are “midpoints” (particularly JSDoc) that make sense even on projects where TypeScript would be too heavy.

JavaScript Doppelgänger 👬

Let’s check out the following file math.ts:

 
const age = 35
const votingAge = 18

if (age > votingAge) console.log('You can vote!')

It’s some fairly simple code and … it looks exactly like JavaScript. But, because I put a .ts as the file extension, it’s technically TypeScript! I couldn’t, however, technically execute this file inside of a web browser, because web browsers can’t run TypeScript without compiling it to JavaScript. Fortunately, it’s not so hard to compile it. If I run:

npm install -g typescript

Then I’ll install TypeScript on my computer. Then, I can run:

tsc math.ts --strict

When I do, I’ll get a new file called math.js. Notice that this is a normal JavaScript file, and the contents will be … exactly the same. Yes, TypeScript and JavaScript often look identical.

The only real difference would be behind the scenes. JavaScript doesn’t really know in advance that the variables age and votingAge are numbers. TypeScript, on the other hand, does, and it will give me compiler errors if I try to do impossible things. Note that the --strict flag ensures that TypeScript will raise errors if I’ve made type mistakes.

Let’s say I have a file like this:

 
const firstName = "Mary";
const info = { age: 25, occupation: "Coder" };

console.log(firstName * info);

In JavaScript, I would get an error when I try to run this code, because there’s not really any way to multiply a string an an object together. But, in TypeScript, I would get an error when I try to compile this code to JavaScript (The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.). As a result, I can prevent any type-related errors from creeping into my project without even having to run the code!

But, how did TypeScript know the types of each of my variables? The answer is that it’s pretty good at automatically inferring the types of new variables. When I say const age = 35, it knows age is a number because of the value I assigned it. So, when writing a “script” (a few lines of code that don’t make use of functions), the TypeScript compiler can be helpful in preventing errors, but our code will often look exactly the same between TypeScript and JavaScript.

Functional Programming

Sometimes, however, the TypeScript compiler won’t be able automatically infer the type of a variable. This happens particularly when working with functions. And, if you’ve ever written JavaScript …….. you’ve written functions 💪

Take for example a simple function like this:

 
const sum = (a, b) => a + b

Unless we tell the TypeScript compiler explicitly, it will have no way of knowing that the parameters a and b should be numbers, nor that the function sum should return a number. As a result, the two parameters automatically have a type that TypeScript calls any.

In general, it’s bad practice to have function parameters or return values with an any type because we might get unexpected results if we make a mistake during coding. For example, right now, sum("cat", "dog") will return "catdog", which I might not expect if the sum function is only expected to take numbers as arguments! So, in TypeScript I should mark the types of the arguments using the : operator like so:

 
const sum = (a: number, b: number) => a + b

This way, if I try running sum("cat", "dog"), the compiler will complain that the function cannot take string arguments. In this case, I don’t actually have to state the type of the return value since TypeScript is smart and will interpret that a number added to a number is always a number. But, as a general rule, I’d recommend always marking the return values up as well, since then if I make a mistake while coding and accidentally write a function that returns the wrong type, TypeScript will tell me. When I mark the return value up, my function will look like:

 
const sum = (a: number, b: number): number => a + b

TypesScript includes a variety of basic types:

  • string
  • number
  • boolean
  • Unions: you can say a variable may take more than one type by using the “union” operator like number | string.
  • Arrays: You can also state that a variable should take an array of a certain type by writing [] after the type like string[].

I should also mention that you can define variables like const age: number = 18, but it generally isn’t as necessary, since TypeScript can infer that type easily.

A cartoon graphics space scene showing an astronaut exploring space as rockets and planets float around.

Stay on the leading edge of development in 30 minutes per month.

Explore new tech with a fun, scaffolded, 30-minute coding challenge that lands in your inbox once a month.

Guard Clauses

One of the main things about functions that I find ends up changing when I use TypeScript is that I end up writing more if statements. As an example, if I have a function like:

 
const firstTwoLetters = (text: string | null): string => {
  return text.substr(0, 2)
}

If I try compiling, TypeScript will complain because text could be null, and null has no substr function, so I might end up with an error! I can fix the error by writing an if statement:

 
const firstTwoLetters = (text: string | null): string => {
  if (typeof text !== 'string') return ''

  return text.substr(0, 2)
}

In general, checking for all possible edge cases is good practice, and writing such “guard clauses” is something most JavaScript developers do anyway.

But, unlike with JavaScript, with TypeScript I will actually see an error on compile if I forget to account for a particular edge case. So, I usually end up with moar guard clauses when using TypeScript because I’m more cognizant of all possible edge cases, and that means my code is more maintainable and understandable 😽

Objects, Type Aliases, and Extensions, Oh My!

You may have noticed I haven’t mentioned a key JavaScript type, however: objects! Why are objects different from other types? It doesn’t really help us most of the time to know that a variable is an object, because we won’t know what keys it might have or what types their values might be.

That’s why, when we’re expecting an object type, we have to be explicit about outlining its expected shape. TypeScript supports a few different syntaxes for roughly the same purpose, but I generally prefer declaring a type myself. A type alias is a type that I (as a developer) define that can list out keys and values, for example:

 
type User = {
  firstName: string;
  lastName: string;
}

Then, I can write a function:

 
const fullName = (user: User): string => `${user.firstName} ${user.lastName}`

In this case, TypeScript won’t give me any errors because it knows that a user has to have a first and last name as strings. If, however, I try to say user.middleName, it will raise a compiler error for me, since users do not have a middle name.

It’s important to note as well that when I’m declaring a variable with an object value, TypeScript automatically knows its properties and types.

 
const jane = { firstName: 'Jane', lastName: 'Doe' }

Here, jane has the following type:

 
{
	firstName: string
	lastName: string
}

That looks suspiciously like the User type I wrote above! Sure enough, if I try running fullName(jane) and pass the variable as a function argument, then TypeScript confirms that it matches the User type. We didn’t have to explicitly tell TypeScript that jane is a User because its inferred type already matches User.

Now, if jane were missing a firstName or lastName property, I’d get an error at compile time.

Extending Type Aliases

Sometimes you end up with two types of objects, where one has a few more keys. Fortunately, TypeScript lets us keep things DRY:

 
type UserWithSignIn = User & {
  email: string;
  password: string;
}

In this case, a UserWithSignIn will contain firstName, lastName, email, and password keys. This pattern ends up being useful for code refactoring, but it can also come in handy when you want to use a type alias from an external package but make some modifications to fit your use case.

React Props

The situation where type aliases end up being possibly the most useful is with regards to props in component-based front-end frameworks since props usually come in as an object. I’m trying in this demo to steer clear of focusing on one specific framework, but it’s worthwhile to note that in React and related frameworks, type aliases are very useful. For example, if I have a JavaScript component like:

 
const Name = (props) => (
  <h1>
    {props.user.firstName} {props.user.lastName}
  </h1>
)

Then, in TypeScript, I would have to explicitly list out which props I want my component to take:

 
type Props = {
  user: User;
}

const Name = (props: Props): JSX.Element => (
  <h1>
    {props.user.firstName} {props.user.lastName}
  </h1>
)

It can be a bit tedious at times to be so explicit about every prop, but it can prevent me from making all kinds of developer errors, be it typos or unconsidered edge cases. As a result, I find that my code ends up being much clearer and that just the TypeScript itself is almost its own form of documentation about how to use the component (though, of course, not a replacement for well-written docs!).

Data from APIs

All these TypeScript features may seem lovely to you so far. You’re already ready to explicitly declare your function parameter/return types. Yay! But, there’s a common gotcha here that I found rather unrepresented in the docs I’ve read: what about when you don’t actually have control over everything? 🤯

For example, if I declare const jane: User = { firstName: 'Jane', lastName: 'Doe' }, then TypeScript can easily check if jane matches the shape of my User type. But what if I’m getting Jane’s data from an API using a fetch request?

There are two approaches here. One is that I could just assume that the response from the API will be of the shape I expect, like so:

 
// assuming I have a function called fetchUserFromAPI that triggers an API request via HTTP
const jane = (await fetchUserFromAPI()) as User

The as operator is a kind of manual override that tells TypeScript to treat jane as a User and not any, but it doesn’t actually check that this assertion is correct. So, Typescript will simply assume that the response from the request exactly matched our type’s shape. But, this may not always be necessarily true, depending on how the API responds!

To be honest, I think using as means we get far less benefit from using TypeScript. We haven’t actually checked anywhere that the API request really gave us a response that matched our type, so we also haven’t accounted for any potential errors if it didn’t.

As a result, the approach I generally take is to write a function known as a type predicate. In this type of function, we use standard JavaScript to convert whatever object we get (of type unknown, which is TypeScript’s type for when we really have no information about an input) to a format that matches our type. As an example:

 
// Checks if some value is a User.
//
// It determines that by checking if the input is an object
// and has both a "firstName" and "lastName" property.
function isUser(input: unknown): input is User {
  return (
    typeof input === 'object' &&
    input !== null &&
    'firstName' in input &&
    'lastName' in input
  )
}

Then, we could run:

 
// assuming `fetchUserFromAPI` is defined elsewere
const jane = await fetchUserFromAPI()

if (isUser(jane)) {
  // sign in or whatever we plan to do with the fetched data
} else {
  // gracefully handle the error, perhaps showing a popup, or console logging
}

There are more cool features in TypeScript, but I think this covers the basics. In general, if you know JavaScript, you just need to add type annotations to have TypeScript.

The one not-particularly-important exception I should quickly note is the Enum type, which is an addition to the language that includes features not natively present in JavaScript. The enum type lets you store a value that can have only a limited range of options (for example, “saved”, “loading”, and “error”). But, similar behavior can always be accomplished with either strings or integers in normal JavaScript, so it’s not really necessary to use enums unless you happen to like the syntax.

Pros and Cons of TypeScript vs. JavaScript

When does TypeScript really help?

So, now you’re familiar with the basics of TypeScript. But when does it make sense to use it?

  • Big projects: Quite simply, when you’re dealing with a large number of files and features, it’s easy for a little bug to get buried somewhere in a massive codebase. TypeScript is really good at catching these types of errors. Along with automated testing, it can help us guarantee the stability of a large codebase.
  • Complex data: A project doesn’t have to necessarily be large to be complex. If a project deals with a number of complex data types (i.e., making requests to a database table with a large number of columns), then I’ve found that TypeScript helps me keep track of everything. TypeScript also helps to document the expected inputs and outputs of every function, which makes it easier for other developers to join in on a project with high complexity.
  • Accessibility: When using TypeScript for front-end development, TypeScript can help identify common accessibility errors, like forgetting alt text on an image. (Check out my 3-part series on accessibility in Next.js for more.)
  • Documentation: TypeScript doesn’t replace writing good documentation. But it does mean that every function will have the types of its parameters and return value documented. Modern text editors like VSCode and Neovim can even display this information as you type. Furthermore, TSDoc provides a standard for writing documentation in TypeScript. So, for projects where collaboration is important, TypeScript can help regularize how everyone writes code so that it’s more easily understandable within the team.
  • Error reduction: For some projects, errors are unacceptable. For example, if we’re dealing with sensitive data like a bank account. In these cases, even if TypeScript doesn’t replace automated testing, it can help prevent developer error and account for edge cases we may forget to test. So, it’s often worth it when bugs are not allowed.

In essence, I think TypeScript is particularly useful for preventing “developer error,” like typos or edge cases that we may not anticipate. The larger, more complex, or more critically important our codebase gets, the more TypeScript can help us identify developer error.

When is TypeScript a pain?

  • Small projects: If you’re just building something small ‘n’ simple, TypeScript may not help much. The additional overhead to add the TypeScript compiler probably just isn’t worth it. Sometimes, setting it up and debugging Typescript errors takes more time than solving any JavaScript bugs.
  • Added complexity: Browsers can’t run TypeScript directly. So, if you’re planning on using it for front-end development, you’ll likely need to use webpack or a similar bundler to compile your assets. If you’d otherwise be fine just using vanilla JavaScript, this additional setup step may be unnecessary.
  • Collaboration: Adding TypeScript introduces an additional layer of knowledge other developers will need to have to collaborate on the project. Many developers know it already, and experienced JavaScript developers will have an easy time picking it up. But, if you want to make your project approachable for a junior developer, TypeScript may detract from the code’s readability for beginners.
  • Duck typing: Certain types of meta-programming may actually take advantage of JavaScript’s flexible types, and TypeScript may make doing so more difficult. It’s certainly not impossible to meta-program with TypeScript, but it may be even more complicated than JavaScript and require weird workarounds.

What is duck typing?

Duck typing refers to the concept that “if it walks like a duck and quacks like a duck, it is a duck.” For example, in JavaScript, you can use the + operator to add numbers but you can also use the same operator with strings. So, we can write expressions that will work fine for either type 🦆

Old packages: Usually NPM packages support TypeScript quite well, but when they don’t it sucks. 🤯 I’ve had a few situations where a package claimed it supported TypeScript, but where it actually had some obscure TypeScript-related bugs buried deep inside the package. As a result, I wasted a lot of time trying to resolve problems that didn’t really exist. If a package is constantly maintained and widely used, you’ll be fine. But if it’s an older package with minimal community need/support, weird things can and do happen.

JSDoc: The Middle Ground

So, what should you do if you’ve decided that the cons outweigh the pros? You don’t have to give up on TypeScript-like features entirely! Of course, I should mention that TypeScript isn’t all-or-nothing. Particularly if you’re already using a bundler like webpack, you can import TypeScript files in a project just like if they were any other format.

But, let’s say you’re not using a bundler, and you don’t want to introduce all the overhead to include TypeScript. Fortunately, there’s a “middle ground” solution that lets you take advantage of many of TypeScript’s benefits with few of the cons: JSDoc.

JSDoc is a standardized way of writing comments in your JavaScript, mostly used to document functions or classes. It works entirely with standard JavaScript comments, so there’s no need for a compiler or really any technical overhead. It includes some special “block tags” with @ symbols in front of them, including @param and @returns, so our sum function from before would look like:

 
/**
 * @function sum
 * @description Takes two numbers and returns them added together.
 * @param {number} a - The first number.
 * @param {number} b - The second number.
 * @returns {number}
 */
const sum = (a, b) => a + b

I love using JSDoc, furthermore, because it lets us use a new terminal command jsdoc sum.js (assuming our function is saved in the file sum.js), which will generate a pretty webpage with all our documentation nicely formatted. So, when I work on larger projects, I try to document every function and then generate a centralized website for all the documentation.

JSDoc offers benefits beyond encouraging you to write good documentation: text editors can often integrate with JSDoc and show you your documentation as you’re typing.

But, you won’t get the “more advanced” features of TypeScript. For example, since there’s no compiler involved, you won’t be alerted to type errors in the code. You also won’t get TypeScript-specific operators like as, or the ability to declare type aliases (although you can document an Object type’s keys through destructuring).

It’s worth noting that there’s also a standard called TSDoc, which is the same thing if you’re working with TypeScript.

Final Thoughts

If you’re already familiar with JavaScript, TypeScript will look familiar. Sometimes, in fact, the two will look exactly the same. Otherwise, TypeScript will require us to add some type annotations to our code, particularly surrounding function parameters and return values. Though it definitely requires some learning, it’s not as complex as learning a whole new language, and it has a lot of benefits.

For large projects or projects with complex data, TypeScript can help prevent developer error. If I misspell a variable or forget to include alt text, TypeScript can find the problem immediately. So, as codebases get larger, it can save time spent on debugging.

In smaller codebases, however, including TypeScript may simply be overkill. It may not be necessary to introduce a compiler and a whole new level of technical complexity. That being said, it’s possibly to gain some of TypeScript’s benefits by relying on JSDoc. Although JSDoc can’t give us all of TypeScript’s power, it’s a great way of writing documentation generally, and it provides some of the benefits with almost none of the downsides.

And, even if you don’t end up using TypeScript so much personally, I think it’s good to be aware of and good for the ecosystem. The next time you install an NPM package that’s built in TypeScript, you may notice that your text editor automatically knows which function you’re using and what arguments it should take. This kind of developer functionality is useful even in plain JavaScript projects 🚀

Alexander Dubovoy

Alexander Dubovoy

Originally from the San Francisco Bay Area, Alexander Dubovoy is a Berlin-based coder and musician. He graduated from Yale University in May 2016, where he wrote an award-winning thesis on the history of jazz in the Soviet Union. Since graduating, he has worked as a freelance web developer. He teaches at Le Wagon, an international coding bootcamp, and loves helping students with tricky technical problems. He also manages an active performance schedule as an improvising musician. He loves to combine his passions for technology and music, particularly through his work at Groupmuse, a cooperative concert-presenting organization.

More posts