DEV Community

Cover image for Week 12: Typescript Magic!
Nikhil Sharma
Nikhil Sharma

Posted on

Week 12: Typescript Magic!

Last week, we talked about how Prisma helps us in simplifying our DB queries, but as a project grows, we face another massive challenge: consistency. When you have dozens of files and data moving between them, it's very easy to lose track of what "shape" an object has.

In plain JavaScript, you might call a property user_id in your database logic, userId in your controller, and just id in your frontend. In a small project, you can keep that in your head. In a production-level app?

It’s a straight-up disaster waiting to happen. You won't know something is broken until the code actually runs and crashes with the dreaded Cannot read property 'id' of undefined.

The solution is TypeScript. It’s not just "JavaScript with types"; it’s a high-tech safety net. It creates a development environment where the editor itself understands your data structures. It ensures that if you change a property name in one place, the rest of your app doesn't quietly crumble—it screams at you until you fix it.

1a. Setting Up the Environment📎

Unlike JavaScript, which Node.js or a browser can read directly, TypeScript needs to be "transpiled." Think of it as a pre-processing step where your type-heavy TS code is stripped down into clean, performant JS that engines can actually execute. To get started, the setup involves a few key commands that I now have on muscle memory:

# Initialize a node project to manage dependencies
npm init -y

# Install TypeScript as a dev dependency (we don't need it in production)
npm install typescript --save-dev

# Initialize the TypeScript compiler and create the config
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

This generates a tsconfig.json file. This file is the "brain" of your project. It’s where you tell the compiler exactly how strict you want to be, which version of JavaScript to target, and where to look for your files.

1b. Organizing with rootDir and outDir

In a professional workflow, you never want your source code (the stuff you edit) and the distribution code (the stuff that runs) mixed together. It makes version control a nightmare and the project structure confusing.

To solve this, we dive into the tsconfig.json and configure two crucial settings:

  • rootDir: This points the compiler to our source folder. Usually, we set this to ./src. This is where all our .ts files live.
  • outDir: This tells the compiler where to dump the generated JavaScript. We usually use ./dist (distribution) or ./build.

By setting these up, your workflow becomes streamlined: you write code in src, run npx tsc, and TypeScript replicates your entire folder structure inside dist, but converted to JavaScript. This keeps your project clean and ready for deployment.

2. Beyond Basics: Interfaces, Enums, and Generics

A. The Power of Interfaces

Interfaces enforce a "contract." If a blueprint says a house needs four windows, TypeScript won't let you build it with three.

export interface User {
    id: string;
    username: string;
    email: string;
    role: UserRole; // Using an Enum
    tags?: string[]; // Optional property
}

Enter fullscreen mode Exit fullscreen mode

B. Enums: Eliminating "Magic Strings"

Don't use strings like "admin" or "guest" throughout your code. Use Enums to create a single source of truth for constant values.

enum UserRole {
    Admin = "ADMIN",
    Moderator = "MODERATOR",
    User = "USER"
}

const myUser: User = {
    id: "1",
    username: "Nikhil_Dev",
    email: "nikhil@example.com",
    role: UserRole.Admin // Type-safe and discoverable
};

Enter fullscreen mode Exit fullscreen mode

C. Generics: Writing Reusable Logic

Generics allow you to create components that work over a variety of types rather than a single one. This is crucial for API responses.

interface ApiResponse<T> {
    status: number;
    data: T;
    error?: string;
}

// Now we can reuse this for Users, Products, or Orders
const userResponse: ApiResponse<User> = {
    status: 200,
    data: myUser
};

Enter fullscreen mode Exit fullscreen mode

3. End-to-End Type Safety (Frontend + Backend)

The "Holy Grail" of development is sharing types across your stack. By exporting interfaces from a shared folder, your Frontend knows exactly what the Backend is sending.

Shared Function Signatures

Imagine a function that updates a user profile. In TypeScript, we can ensure the payload matches the expected structure before the API call is even made.

type UpdateUserPayload = Pick<User, "username" | "tags">;

async function updateUser(id: string, payload: UpdateUserPayload): Promise<void> {
    // TypeScript ensures payload only contains username or tags
    await fetch(`/api/user/${id}`, {
        method: 'POST',
        body: JSON.stringify(payload)
    });
}

Enter fullscreen mode Exit fullscreen mode

New things I learnt this week🔄

  • The Power of strict: true: Turning this on in tsconfig is like playing a game on "Hard Mode." It forces you to handle null and undefined cases explicitly. It’s annoying at first, but it eliminates about 90% of common runtime bugs.
  • Targeting Environments: I learned that I can write modern ES6+ TypeScript but tell the compiler to output older ES5 code if I need to support legacy environments.
  • Type Inference: I realized I don't have to define everything. If I write let name = "Nikhil", TypeScript is smart enough to know it's a string. You only need to be explicit when things get complex.

Wrapping up

While the topics that I covered this week were small, they are crucial and of immense importance when building production-ready apps. If you have any questions or feedback, make sure to comment and let me know!

I'll be back next week with more. Until then, stay consistent!

Top comments (0)