Home
Back to Blogs
TypeScript: Supercharge Your JavaScript with Type Safety

TypeScript: Supercharge Your JavaScript with Type Safety

Murat Hüdavendigâr Öncü
October 16, 2023
6 min
#typescript#javascript#web-development#frontend#programming

Discover how TypeScript adds robust static typing to JavaScript, preventing runtime errors and drastically improving your developer experience.

Last updated on July 18, 2026

JavaScript is fantastic, but as your applications grow, its dynamic nature can sometimes lead to unexpected runtime errors. Enter TypeScript: a strict syntactical superset of JavaScript that adds optional static typing to the language.

In production React and Next.js apps I ship, TypeScript is non-negotiable — it catches the same class of bugs students hit when they first move from tutorials to real components. This post is the short “why and how” I give before we type a full feature.

Why TypeScript?

If you have ever encountered a TypeError: Cannot read properties of undefined in production, you already know why TypeScript is necessary. It catches errors during development (compile-time) before they ever reach your users.

  • Type Safety: Ensure variables hold the exact type of data you expect.
  • Better IDE Support: Get robust autocomplete and intelligent code navigation.
  • Easier Refactoring: Change code with confidence, knowing the compiler will catch broken references.

Basic Types

TypeScript provides several basic types that you can assign to your variables.

// Defining basic types
let username: string = "muratoncu";
let age: number = 28;
let isDeveloper: boolean = true;

Typing Functions

One of the most common sources of bugs in JavaScript is passing the wrong arguments to a function. TypeScript solves this by enforcing parameter types and return types.

// Function with typed parameters and a typed return value
function greetUser(name: string, age: number): string {
  return `Hello ${name}, you are ${age} years old!`;
}

greetUser("Murat", 28); // Works perfectly
// greetUser("Murat", "28"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Interfaces and Custom Types

As we discussed in a previous article, you can define the shape of complex objects using Interfaces or Types. This is where TypeScript truly shines, allowing you to model your business logic accurately.

interface User {
  id: number;
  name: string;
  isActive: boolean;
}

const fetchUser = (id: number): User => {
  // Imagine fetching data from an API here
  return { id, name: "Murat", isActive: true };
};

Union Types and Narrowing

Real data often isn't just one type. A function might accept a string or a number, or an API response might return either a success payload or an error. TypeScript models this with union types, written with a pipe:

function formatId(id: string | number): string {
  return `ID-${id}`;
}

Inside a function, before you can use a value in a type-specific way, TypeScript requires you to narrow the union down using a runtime check:

function printLength(value: string | string[]) {
  if (typeof value === "string") {
    console.log(value.length); // TypeScript knows value is a string here
  } else {
    console.log(value.length); // TypeScript knows value is a string[] here
  }
}

The typeof check isn't just a JavaScript runtime check — the TypeScript compiler tracks it and narrows the type inside each branch accordingly. This is why you can't accidentally call a string-only method on something that might be an array; the compiler stops you before the code ever runs.

Generics: Writing Reusable, Type-Safe Functions

Without generics, a reusable utility function either loses type information (falling back to any) or has to be duplicated for every type you use it with. Generics let a function or type be parameterized by a type variable, filled in at the call site:

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

const firstNumber = firstItem([1, 2, 3]);       // inferred as number | undefined
const firstName = firstItem(["Murat", "Ada"]);  // inferred as string | undefined

T is a placeholder that TypeScript fills in based on what you pass in — no any, no duplication, and the return type is still fully checked. Generics show up constantly in real codebases: React's useState<T>(), array methods, and most data-fetching utilities are generic for exactly this reason.

Utility Types: Reshaping Types Without Rewriting Them

TypeScript ships a set of built-in utility types that transform an existing type instead of making you redefine it from scratch. Given the User interface from earlier:

interface User {
  id: number;
  name: string;
  isActive: boolean;
}

type PartialUser = Partial<User>;      // every field becomes optional — useful for update payloads
type UserPreview = Pick<User, "id" | "name">;  // only id and name
type UserWithoutId = Omit<User, "id">; // everything except id

Partial<User> is especially common for update functions — a PATCH /users/:id endpoint typically accepts any subset of fields, and Partial<User> expresses exactly that without a second, hand-maintained interface.

function updateUser(id: number, changes: Partial<User>) {
  // changes might contain just { isActive: false }, or any subset of User's fields
}

Enabling Strict Mode

A newly initialized TypeScript project without configuration will let far more slip through than you'd expect — including implicit any types on untyped parameters. The single highest-leverage setting in tsconfig.json is strict:

{
  "compilerOptions": {
    "strict": true
  }
}

strict bundles several checks, most notably strictNullChecks (which forces you to explicitly handle null/undefined rather than assuming a value is always present) and noImplicitAny (which errors on parameters TypeScript can't infer a type for, instead of silently treating them as any). Turning this on for a new project costs nothing; turning it on for an existing untyped codebase can surface a lot of latent bugs at once — which is exactly the point.

Common Pitfalls When Adopting TypeScript

  • Overusing any to silence errors. Typing something any disables type checking for that value entirely — it's an escape hatch, not a type. Reaching for any every time the compiler complains defeats the purpose of adopting TypeScript in the first place. unknown is almost always the safer choice when you genuinely don't know the type yet, because it forces you to narrow before using the value.
  • Typing the return value of JSON.parse() and trusting it blindly. JSON.parse() returns any by design — TypeScript cannot verify that the JSON string actually matches the shape you're casting it to. Casting the result as User doesn't validate anything at runtime; a mismatched API response will pass the type checker and still fail (or misbehave) in production. For real safety at that boundary, pair TypeScript with a runtime validation library.
  • Confusing compile-time types with runtime behavior. Types are erased entirely when TypeScript compiles to JavaScript — there is no typeof user === "User" check at runtime, because interfaces don't exist once compiled. TypeScript prevents you from writing code that violates its contracts, but it cannot stop malformed data arriving from outside your codebase (an API, localStorage, user input) from violating them anyway.
  • Widening literal types by accident. let status = "active" infers the type as the general string, not the literal "active". If you need a variable restricted to a specific set of string values, declare it explicitly: let status: "active" | "inactive" = "active". Otherwise, code elsewhere that expects only those two values will happily accept any string, and the compiler won't catch the mismatch until it's used somewhere stricter.

TypeScript vs. Plain JSDoc Comments

If your team can't fully adopt a TypeScript build step, JSDoc comments give you a lighter-weight subset of the same benefit — many editors, including VS Code, read JSDoc annotations on plain .js files and provide the same autocomplete and basic type checking:

/**
 * @param {string} name
 * @param {number} age
 * @returns {string}
 */
function greetUser(name, age) {
  return `Hello ${name}, you are ${age} years old!`;
}

This is a reasonable stepping stone for legacy JavaScript codebases that can't migrate file extensions overnight, but it doesn't get you generics, utility types, or the discipline of a dedicated tsconfig.json with strict mode enabled. Treat JSDoc as a bridge, not a permanent substitute, if the goal is the full safety net TypeScript provides.

Conclusion

Adopting TypeScript might feel like a steep learning curve initially, but the long-term benefits are undeniable. By providing a safety net — from basic types through unions, generics, and utility types — it allows developers to write cleaner, more reliable, and self-documenting code. It does not replace JavaScript; it simply supercharges it.

Back to all posts