How TypeScript Helps You Write Better Code

TypeScript, the statically typed superset of JavaScript, has exploded in popularity in recent years. According to the 2021 Stack Overflow Developer Survey, TypeScript is now the 2nd most loved language (behind only Rust) and the 4th most wanted language. The RedMonk Programming Language Rankings place TypeScript in the top 10 most popular languages overall.

So why are so many developers falling in love with TypeScript? In short, it‘s because TypeScript helps you write better code. By adding static typing, interfaces, enums, and other features on top of JavaScript, TypeScript catches errors early, makes code more readable and maintainable, and enables powerful tooling and refactoring capabilities. Let‘s dive into the details.

Understanding TypeScript

At its core, TypeScript is JavaScript with type annotations. All valid JavaScript code is also valid TypeScript code. This means you can easily integrate TypeScript into an existing JavaScript project, gradually adding type annotations and leveraging TypeScript‘s features as you go.

Here are some of TypeScript‘s key features:

Static Typing

TypeScript allows you to specify types for variables, function parameters, and return values:

let count: number = 0;
function greet(name: string): string { 
    return `Hello, ${name}!`;
}

Type Inference

TypeScript can often infer types for you based on the assigned value or the values passed to a function:

let message = "Hello World"; // inferred as string
function add(a: number, b: number) {
    return a + b; // return type inferred as number
}

In the add function, TypeScript infers that a and b are numbers because they‘re added together, and it infers that the function returns a number.

Interfaces

Interfaces define the shape of an object:

interface Person {
    name: string;
    age: number;
}

Enums

Enums define a set of named constants:

enum Color {
    Red,
    Green, 
    Blue,
}

Access Modifiers

TypeScript supports public, private, and protected access modifiers for class members:

class BankAccount {
    private balance: number;

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }
}

Strict Null Checks

With strictNullChecks enabled, TypeScript distinguishes between values that can be null/undefined and those that can‘t:

let name: string = null; // Error: Type ‘null‘ is not assignable to type ‘string‘
let age: number | null = null; // OK

Generics

Generics allow writing reusable code that works with multiple types:

function identity<T>(arg: T): T {
    return arg;
}

Decorators

Decorators are a way to annotate and modify classes and properties at design time:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class BankAccount {
    // ...
}

Async/await and Promise Types

TypeScript understands async/await syntax and can infer the return type of async functions:

async function getUser(id: number): Promise<User> {
    // ...
}

Structural vs Nominal Typing

TypeScript uses structural typing, also known as "duck typing". This means that if two objects have the same shape, they are considered to be of the same type, even if they don‘t have an explicit relationship:

interface Point {
    x: number;
    y: number;
}

class CartesianPoint {
    x: number;
    y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

let point: Point = new CartesianPoint(1, 2); // OK

Even though CartesianPoint doesn‘t explicitly implement Point, it has the same shape (an x and a y property of type number), so it can be assigned to a variable of type Point.

The Problems with JavaScript‘s Type System

JavaScript‘s dynamic, weak typing is both a blessing and a curse. On one hand, it allows for rapid prototyping and flexibility. On the other hand, it can lead to bugs and maintainability issues, especially in large codebases:

  • Runtime errors: Without static typing, type mismatches aren‘t caught until runtime. A function expecting a number might be called with a string, leading to unexpected behavior or crashes.

  • Difficulty understanding code: Looking at a JavaScript function signature, it‘s not immediately clear what types of arguments it expects or what it returns. This makes the code harder to understand and maintain.

  • Refactoring challenges: Changing a function‘s parameters or return type in JavaScript requires manually finding and updating all call sites. This is error-prone and can lead to bugs.

How TypeScript Solves These Problems

TypeScript addresses these issues by introducing static typing and other features that promote correctness and maintainability.

Catching Bugs Early

With TypeScript‘s static typing, many common bugs are caught at compile time:

function add(a: number, b: number): number {
    return a + b;
}

add(1, ‘2‘); // Error: Argument of type ‘string‘ is not assignable to parameter of type ‘number‘.

Self-Documenting Code

TypeScript type annotations serve as a form of documentation, making it clear what types a function expects and returns:

function greet(name: string): string {
    return `Hello, ${name}!`;
}

Just by looking at the function signature, we know it takes a string and returns a string.

Clear Contracts with Interfaces

Interfaces define the shape of an object, acting as a contract between the producer and consumer of the object:

interface User {
    id: number;
    name: string;
    email: string;
}

function getUser(id: number): Promise<User> {
    // ...
}

The getUser function promises to return an object that conforms to the User interface.

Ensuring Correctness

TypeScript‘s enums, strict null checks, and other features help ensure correctness:

enum Role {
    Admin,
    User,
}

function authorize(user: User, role: Role) {
    if (user.role !== role) {
        throw new Error(‘Unauthorized‘);
    }
}

Reusable Code with Generics

Generics allow writing reusable code that works with multiple types:

class Stack<T> {
    private items: T[] = [];

    push(item: T) {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }
}

const numberStack = new Stack<number>();
const stringStack = new Stack<string>();

Enhanced Classes

TypeScript‘s access modifiers and decorators enhance JavaScript classes:

class BankAccount {
    private balance: number;

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }

    @logMethod
    deposit(amount: number) {
        this.balance += amount;
    }
}

Async Code with Async/Await and Promise Types

TypeScript understands async/await and can infer the return type of async functions:

async function getUsers(): Promise<User[]> {
    const response = await fetch(‘/api/users‘);
    return await response.json();
}

Confident Refactoring

TypeScript‘s static typing and tooling support make refactoring a breeze. IDEs can accurately find all references to a symbol, rename it project-wide, and catch any resulting type errors.

Adopting TypeScript

One of TypeScript‘s strengths is its gradual adoptability. You can start by adding type annotations to a few files in an existing JavaScript project, and gradually expand the typed portion of your codebase.

TypeScript also integrates well with popular build tools and frameworks. Many frameworks, like Angular and NestJS, use TypeScript by default. Others, like React and Vue, have strong TypeScript support.

TypeScript can be used in both frontend and backend development. On the frontend, it can be compiled to JavaScript for running in the browser. On the backend, it can be used with Node.js, with the ts-node package allowing direct execution of TypeScript files.

TypeScript in the Real World

TypeScript has seen significant adoption across the industry. Large companies like Microsoft, Google, Airbnb, Slack, and Shopify use TypeScript for major projects.

"TypeScript allows us to scale our development process as our app grows in complexity. Static typing gives us the confidence to refactor and evolve our code base." – Shopify Engineering

Many popular open source projects, like Vue, Jest, Prisma, and Nest, are also written in TypeScript.

"By embedding types into our code and enabling static type checking across our entire codebase, TypeScript acts as a catalyst that enables us to scale up our practices for growing and maintaining a large codebase with confidence." – Vue.js 3.0 Announcement

Getting Started with TypeScript

Getting started with TypeScript is straightforward. First, install the TypeScript compiler:

npm install -g typescript

Then, create a .ts file, write some TypeScript code, and compile it to JavaScript:

tsc myFile.ts

There are also many great learning resources:

Conclusion

TypeScript brings the power of static typing and other advanced features to JavaScript, helping developers write more correct, maintainable, and scalable code. By catching errors early, expressing clear contracts, and enabling confident refactoring, TypeScript has become an invaluable tool for JavaScript developers.

Whether you‘re working on a small project or a large enterprise application, on the frontend or the backend, TypeScript can help you write better code. Its gradual adoptability and strong tooling and framework support make it accessible for developers and teams of all sizes.

If you‘re not using TypeScript yet, now is a great time to start. The benefits are clear, and the community and tooling are stronger than ever. Give TypeScript a try in your next project – you might be surprised at how much it improves your development experience.

Similar Posts