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 🚂
In this article, I’ll compare the features, pros, and cons of TypeScript vs. JavaScript. I’ll review key syntax concepts and explain how TypeScript would change existing JavaScript codebases to help you determine if TypeScript is a good fit for you.
Typescript vs. JavaScript - what’s the difference?
Here’s the TL;DR:
TypeScript is a typed superset of JavaScript developed by Microsoft that compiles to plain JavaScript. Unlike JavaScript, TypeScript adds optional static typing and other features like interfaces, generics, and enums to enable stronger tooling and code quality at scale.
If you are considering learning TypeScript, the good news is that you don’t have to learn millions of new concepts to test it 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.
Here’s a quick comparison of the features:
Has compile-time static type checking with unions, tuples, interfaces, and more
Dynamically typed
Compiles to plain JavaScript
No compilation step
Requires tools that understand TypeScript (tools like ESLint and Prettier do)
Vast availability of tools like ESLint and Prettier.
Robust IDE support and autocomplete
More basic IDE support
Adds syntax to declare static types
Uses standard JavaScript syntax
Growing steadily
Immensely popular
Moderate initial learning curve
Very easy to start
Advantages - when to use TypeScript vs. JavaScript
So what are the advantages of TypeScript over JavaScript, and 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 necessarily have to 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.
Disadvantages - when is using TypeScript a pain?
In other cases, using TypeScript over JavaScript may actually be a disadvantage or may simply be overkill. Let’s take a look at some instances where you may not want to use it 👇
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.
Comparing TypeScript vs. JavaScript syntax
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.
Working with functions in TypeScript
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 likestring[]
.
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.
Stay on the leading edge of development with a monthly coding challenge.
Explore new tech with a fun, scaffolded, coding challenge that lands in your inbox once a month. You'll get all of the learning with minimal spin-up.
Using TypeScript type guards
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 😽
What about TypeScript objects?
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!).
Using TypeScript with 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.
JSDoc: the middle ground
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.
TypeScript vs. JavaScript - which is better?
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 🚀
Knowledge Check!
True or false - TypeScript compiles to plain JavaScript?