Should you choose TypeScript over JavaScript?

One of software development's most lively debates has been whether static typing is preferable to dynamic typing.

Static typing involves using a tool to check code types at implementation time—you write code, and a type-checker tool looks for any types that aren’t compatible without compiling or executing the code. With dynamic typing, the software checks if types are compatible when the code is executed.

While it's better to receive an error notification earlier than later, static typing generates additional work when writing the code. It requires the addition of the correct type annotations. In particular, older languages like Java or C++ have placed considerable burdens on the developer, while their static type checks often fail to find crucial bugs. For example, Java requires verbose type annotations, while generic annotations can lead to the loss of type information at runtime.

With TypeScript, static typing has entered the JavaScript ecosystem. It is a programming language created in 2012 by Microsoft that compiles to JavaScript. TypeScript is similar to JavaScript, with differences in type annotations inside the code.

In the following example, the TypeScript version has type annotations that tell the developer what types the add function expects.

// JavaScript 
function add(x, y) {
return x + y
}

// TypeScript
function add(x: number, y: number) {
return x + y
}

JavaScript originally started as a minor language in browsers, allowing creators to add interactive features to websites. Today, fully fledged client-side applications are written in JavaScript. TypeScript helps with these big code bases and makes them more manageable.

Why TypeScript?

This article will introduce TypeScript and help determine when to use it for a project. We’ll start with the reasons for using TypeScript in the first place. After all, it involves a compilation step and requires developers to rethink how they would implement different features.

Planning the software

JavaScript only allows defining the name of a function and how many arguments it accepts. With TypeScript, you can also determine the type of the arguments and what type the function returns.

Let’s look at the following example of a Logger class:

// JavaScript 
class Logger {
#serverUrl
constructor(serverUrl) {
this.serverUrl = serverUrl
}
log({logLevel, message}) {
fetch(this.serverUrl, {
method: "post",
body: JSON.stringify({logLevel, message})
})
}
}

// TypeScript
interface LogMessage {
logLevel: "info" | "danger"
message: string
}
class Logger {
#serverUrl: string
constructor(serverUrl: string) {
this.serverUrl = serverUrl
}
log({logLevel, message}: LogMessage): void {
fetch(this.serverUrl, {
method: "post",
body: JSON.stringify({logLevel, message})
})
}
}

In the JavaScript version, there’s no way to define what types of data the methods accept. In the planning stage, where the implementation of these methods is not yet defined, you need to use custom means to explain the desired types.

With TypeScript, the function signature is easier to grasp thanks to interfaces that reveal significantly more details than the JavaScript version.

Data and API modeling tools can export more information about the data structures with TypeScript than JavaScript, making these tools more powerful and tightly integrated with your codebase.

Writing code

TypeScript can reveal potential edge cases when writing code—for example, when using data that an asynchronous function will set in the future.

//JavaScript 
class Profile {
#name
#birthday

async load() {
const {name, birthday} = await fetchProfile()
this.name = name
this.birthday = birthday
}

getAge() {
const date =
new Date(Date.now() - this.birthday.getTime());
return Math.abs(date.getUTCFullYear() - 1970);
}
}

// TypeScript
class Profile {
#name?: String
#birthday?: Date

async load() {
const {name, birthday} = await fetchProfile()
this.name = name
this.birthday = birthday
}

getAge() {
const date = new Date(Date.now() - this.birthday.getTime());
return Math.abs(date.getUTCFullYear() - 1970);
}
}

In the JavaScript example, you wouldn’t get an error until calling the getAge method before the load method. This will work fine most of the time, but a new developer could potentially forget to call the load method or check the optional variable before using it and thus risk the system crashing.

With TypeScript, implementing the getAge method will lead to an error. The this.birthday field could be undefined because we have marked it as optional with the ? operator.

Using third-party libraries

A big part of software development today is using third-party software to build your own on top of it. TypeScript provides more granular information about the API of third-party libraries than JavaScript would—it’s similar to having an API reference, but inside your IDE.

TypeScript error Fig. 1: TypeScript error

In the example in fig. 1, TypeScript provides two pieces of information for the Lib class: that its constructor requires one string argument and that the constructor is private so a call to it will result in an error at runtime.

Reading code

Opening a fellow developer’s code can reveal baffling implementations—not unlike looking at one’s own code from months ago. Developers tend to avoid comments, but TypeScript can help.

There is additional information inside the codebase that can clear things up. Spotting a function that accepts an object doesn’t show much, but seeing the actual types of all the object’s fields leads to a clearer picture.

Let’s look at the following example:

interface LibConfig { 
key: string
limit?: number
}
export class Lib {
private constructor(path: string) {}

static create(path: string, config?: LibConfig) {
return new Lib(path)
}
}

Since the creator of the Lib class used TypeScript, we can check the LibConfig interface to see what options this config object offers. If the developer had used JavaScript, we would now need to check the implementation of the create method to see all the options. In the above case, this isn’t a problem, but with a big method the process can quickly get tedious.

Comments are helpful but they wouldn’t be auto-completed by the IDE.

Refactoring code

This is where TypeScript shines. TypeScript’s refactoring capabilities are a considerable asset when creating huge codebases. JavaScript usually ignores the variable type and lets you pass the content of variables around without looking at it; after all, an object is an object.

With TypeScript, you can give the object type information alongside. Change a field, and TypeScript will display all field usage around the codebase; furthermore, IDEs can use that information to refactor all these locations automatically.

In the next example, we’ll change the limit field of the previous example from optional to required:

interface LibConfig { 
key: string
limit: number
}

We will get an error in every file that uses this interface.

TypeScript refactoring error Fig. 2: TypeScript refactoring error

The error in Figure 2 clearly shows that the object being passed is missing a mandatory field limit. If the create method was used in hundreds of files, this error will be tremendously helpful—these files will get automatically checked and marked with an error so the developer doesn’t forget to fix each problem.

Automated testing

TypeScript isn’t perfect. It can’t capture all errors; however, some tests for a JavaScript implementation often become obsolete. For example, there is no need to test if you have accidentally accessed an optional field in a parameter nor how a function reacts when supplied with string arguments instead of numbers.

Why JavaScript?

For some projects, JavaScript might be the better choice. It’s faster to get started with, and without a compilation step, it’s well-suited for faster iterations. But the question remains whether its benefits outweigh its drawbacks, such as the potential errors that result from missing static typing.

More developers

JavaScript is older than TypeScript and has acquired a massive following over the years. If finding talent for a project is the main concern, JavaScript will be a safer bet than TypeScript. The simpler syntax is also easier to understand for junior developers, so onboarding is smoother.

No compilation required

JavaScript has the advantage of running directly in a JavaScript runtime, providing faster performance and a more seamless developer experience. Additionally, with frequent builds deployed daily, the cost of running compilation steps in a continuous integration pipeline can be reduced with JavaScript.

More concise code

Developers with extensive experience in writing JavaScript might feel that TypeScript slows them down. While both allow the same code style, code that is not JavaScript idiomatic is often easier to write, which leads to different implementations in TypeScript. JavaScript is a valid TypeScript syntax, but if the correct types are added later, TypeScript might require excessive type annotations to ensure correct implementation.

Also, TypeScript might prevent you from writing certain code because it sees an edge case that could lead to an error, even if it’s all but certain not to occur because you have checked for it in a different part of the application. This can make using TypeScript a frustrating experience.

Easier to maintain

Tools break and need maintenance— the fewer tools needed to get the job done, the better. Since no compilation step is required, building with JavaScript can lead to a lightweight development process: You can execute the code as you see it in the editor. This can make searching for a bug easier, as there are fewer steps between the code and the application that could cause the problem.

JavaScript is still receiving updates

JavaScript has changed quite a lot over the past decade. While it originally served as a small scripting language for browsers, JavaScript is now used for large applications.

This broader use has brought certain challenges: A larger code base requires more organized structure, but JavaScript doesn’t have a module system. And while browser applications interact with the network quite often, JavaScript only supplies developers with callbacks, which makes error handling tedious.

These problems prompted the JavaScript community to propose changes to the language over the years. A native module system was added to the language, crafted to work with modules distributed via the network and loaded on demand.

Asynchronous functions were added to make asynchronous programming look more like synchronous programming, turning JavaScript into a good fit for software working with a network.

Adopt gradually

While starting a new project with TypeScript is more straightforward than adding it later in the process, this isn’t always an option—and it’s not a requirement, either.

TypeScript can be gradually adopted in a codebase. It can check JavaScript code to some extent, and you can add type definitions outside JavaScript to make it easier to use.

In addition, TypeScript understands JSDoc comments that allow you to annotate types in JavaScript comments so the code stays plain JavaScript, with most of the type checking retained.

Summary

TypeScript is a powerful tool for improving JavaScript codebases. It especially shines when using third-party libraries or refactoring your code, providing suggestions in the IDE and greatly improving developer experience.

However, TypeScript comes with drawbacks: It involves additional tooling and adds a compilation step, and as a result it is less suited for work with prototypes or smaller projects. In addition, skilled JavaScript developers might find that TypeScript requires them to write implementations in ways that might seem overly cluttered in JavaScript.

Was this article helpful?

Related Articles

Write For Us

Write for Site24x7 is a special writing program that supports writers who create content for Site24x7 "Learn" portal. Get paid for your writing.

Write For Us

Write for Site24x7 is a special writing program that supports writers who create content for Site24x7 “Learn” portal. Get paid for your writing.

Apply Now
Write For Us