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?