Back to Blogs

JavaScript · TypeScript · Tooling

Why TypeScript? A Practical Guide

JavaScript makes it easy to get started, but as an application grows, it can become difficult to know what shape data has or what a function expects. TypeScript adds a static type checker to JavaScript so many mistakes can be found while you write code—before they become runtime bugs.

1. What is TypeScript?

TypeScript is a programming language built on top of JavaScript. It adds a type system and development tools that help describe the values your program works with. TypeScript code is checked by the TypeScript compiler, then transformed into JavaScript that browsers and JavaScript runtimes can execute.

TypeScript is a superset of JavaScript: valid JavaScript is generally valid TypeScript, and you can adopt it gradually. In practice, modern TypeScript projects use compiler settings and lint rules that encourage more explicit type safety.

// JavaScript
function greet(name) {
    return `Hello, ${name}`;
}

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

In the TypeScript version, name: string describes the parameter, and : string describes the return value. The compiler can report a mistake such as calling greet(42) before the program runs.

2. Why was TypeScript created?

JavaScript was designed to make interactive web pages possible and approachable. Over time, it grew far beyond the small scripts it was first known for: teams began building large applications with many modules, contributors, and long-lived codebases.

In a small script, it may be easy to remember what a function expects or what fields an object contains. In a large application, that knowledge is spread across files and teammates. A function can be changed without every caller being obvious. A response object can have an unexpected shape. A refactor can quietly leave a broken property access behind.

TypeScript was created to address the needs of large-scale JavaScript development: add static analysis and helpful editor tooling while keeping JavaScript's runtime model, ecosystem, and broad compatibility. It was publicly introduced by Microsoft in 2012 and is maintained as an open-source project.

The core idea is not to replace JavaScript's runtime. It is to catch more mistakes before runtime and make code easier for developers and tools to understand.

3. What problems does TypeScript help solve?

Wrong values passed to functions

JavaScript will let a function receive a value of any type unless your code checks it:

// JavaScript
function applyDiscount(price, percentage) {
    return price - price * percentage / 100;
}

applyDiscount("100", "10"); // Coercion can hide the mistake.

TypeScript can document and check the contract:

function applyDiscount(
    price: number,
    percentage: number,
): number {
    return price - (price * percentage) / 100;
}

applyDiscount(100, 10);     // OK
applyDiscount("100", "10"); // Type error

Typos and missing properties

With plain JavaScript, a misspelled property often produces undefined and the failure may surface somewhere later:

// JavaScript
const user = { firstName: "Maya", email: "maya@example.com" };
console.log(user.firstname.toUpperCase()); // undefined, then a runtime error

A declared type lets the editor and compiler identify the typo at the property access:

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

const user: User = {
    firstName: "Maya",
    email: "maya@example.com",
};

console.log(user.firstName.toUpperCase()); // Checked by TypeScript
console.log(user.firstname); // Type error: no such property

Unclear contracts between modules

A function signature can communicate what it accepts and returns without requiring someone to inspect every call site or implementation detail. Types are especially useful at boundaries: API clients, shared utilities, component props, and data models.

Risky refactors

When a type or function contract changes, the compiler can show many affected call sites. This does not prove the refactor is correct, but it helps uncover places that need attention before users encounter them.

Editor and navigation friction

Since types describe code structure, editors can offer more reliable autocomplete, inline documentation, rename operations, go-to-definition, and diagnostics. These features can be valuable even before a project reaches a very large size.

4. JavaScript and TypeScript side by side

Consider a function that formats a person's display name. The JavaScript version works when the object has the expected data, but the contract is implicit:

// JavaScript
function formatName(person) {
    return `${person.firstName} ${person.lastName}`;
}

formatName({
    firstName: "Sam",
    lastName: "Rivera",
});

Add a type to make the contract visible and checked:

// TypeScript
type Person = {
    firstName: string;
    lastName: string;
};

function formatName(person: Person): string {
    return `${person.firstName} ${person.lastName}`;
}

formatName({
    firstName: "Sam",
    lastName: "Rivera",
}); // OK

formatName({ firstName: "Sam" }); // Error: lastName is missing

The compiled JavaScript contains the function and template string, but not the Person type or annotations. They guide checking and tooling; they are not runtime objects.

5. Types you'll use in everyday code

Object types and optional properties

type Product = {
    id: string;
    name: string;
    price: number;
    description?: string;
};

function productLabel(product: Product): string {
    return `${product.name} — $${product.price.toFixed(2)}`;
}

The ? marks a property as optional. Code must account for the possibility that description is absent before using it.

Union types and narrowing

A union says a value can be one of several types. Use checks to narrow it to a specific case:

function formatId(id: string | number): string {
    if (typeof id === "number") {
        return id.toFixed(0);
    }

    return id.toUpperCase();
}

This is safer than choosing a broad type like any, because TypeScript requires you to handle each possibility before using operations that only work for one member.

Literal unions for finite choices

type RequestState = "idle" | "loading" | "success" | "error";

function showState(state: RequestState) {
    if (state === "loading") {
        return "Please wait…";
    }

    return state;
}

showState("pending"); // Type error: not an allowed state

Generics for reusable functions

A generic lets a function preserve the relationship between its input and output types instead of losing that information:

function first<T>(items: T[]): T | undefined {
    return items[0];
}

const firstNumber = first([10, 20, 30]); // number | undefined
const firstName = first(["Ari", "Bo"]);  // string | undefined

Discriminated unions for UI states

A tagged union is useful for representing states that have different data. It can prevent impossible combinations, such as a loading state that also claims to have an error message:

type LoadResult<T> =
    | { status: "loading" }
    | { status: "success"; data: T }
    | { status: "error"; message: string };

function renderResult(result: LoadResult<string[]>): string {
    switch (result.status) {
        case "loading":
            return "Loading…";
        case "success":
            return result.data.join(", ");
        case "error":
            return result.message;
    }
}

6. Practical use cases

Typing component props

In React with TypeScript, a props type documents the component's public interface and enables editor assistance where the component is used:

type GreetingProps = {
    name: string;
    subtitle?: string;
};

function Greeting({ name, subtitle }: GreetingProps) {
    return (
        <section>
            <h1>Hello, {name}</h1>
            {subtitle && <p>{subtitle}</p>}
        </section>
    );
}

Modeling API response data

A type can describe the response your application expects and make downstream code clearer:

type ApiUser = {
    id: number;
    name: string;
    email: string;
};

async function getUser(id: number): Promise<ApiUser> {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
        throw new Error(`Request failed: ${response.status}`);
    }

    return response.json() as Promise<ApiUser>;
}

This annotation describes what the application expects; it does not prove that the server returned that shape. An API response is external data. For robust applications, parse it as unknown and validate it at runtime with checks or a schema-validation library before treating it as an ApiUser.

Safer event and DOM handling

TypeScript can help ensure that a queried element exists and is of the expected kind:

const form = document.querySelector("form");

if (!form) {
    throw new Error("The form element was not found");
}

form.addEventListener("submit", (event) => {
    event.preventDefault();
    // form is known to be an HTMLFormElement here.
});

Sharing types across features

Types can define contracts shared by form validation, API calls, and UI components. For example, a single Product type can keep the cart, product page, and checkout code aligned as the model evolves.

7. Trade-offs, limits, and common myths

TypeScript does not prevent every bug

Types can catch many mistakes, but they cannot prove that a program's business logic is correct. A program may be perfectly type-safe and still calculate the wrong price or show a confusing interface. Tests, reviews, and runtime validation remain important.

Types do not validate external data

A type annotation is not a security boundary. Values from JSON, query strings, local storage, and user input should be considered untrusted until checked at runtime. Type assertions such as value as User tell the compiler to trust you; they do not inspect or transform the value.

There is a learning and tooling cost

Teams need to learn the type system and configure a build or type-check step. Complex types can also become harder to read than the code they are intended to clarify. Prefer types that communicate useful contracts, and avoid making a simple feature unnecessarily abstract.

TypeScript is not a different runtime

TypeScript compiles to JavaScript. It does not make code execute faster by itself, and type annotations are not generally present in the emitted JavaScript. Runtime behavior still depends on the JavaScript output and the APIs it uses.

Be cautious with any

any disables checking for that value and can spread unchecked assumptions through your code. Prefer unknown when a value's type is not known yet; it requires you to narrow or validate the value before using it.

8. How to start using TypeScript

  1. Learn the JavaScript fundamentals first. TypeScript builds on JavaScript rather than replacing its runtime concepts.
  2. Start with a small feature. Add types to function parameters, return values, component props, and data models.
  3. Turn on strict checking. A strict TypeScript configuration catches more unsafe assumptions. Existing JavaScript projects can migrate gradually.
  4. Validate data at boundaries. Treat network responses and user-provided values as unknown until checked.
  5. Use inference where it is clear. You do not need to annotate every local variable; TypeScript can infer many types from their values.

For example, this variable's type is inferred as number, so an explicit annotation is usually unnecessary:

const retryCount = 3; // inferred as number

9. So, why TypeScript?

TypeScript helps make assumptions in JavaScript code visible and checkable. It can catch mismatched values, missing properties, and broken contracts earlier; improve editor tools; and make larger refactors easier to navigate.

It is most useful when its types clarify real relationships in your program—not when every line is covered in annotations for their own sake. Use it alongside tests and runtime validation, and keep the types as simple as the problem allows.