Month 3, Week 4

The TypeScript Era

Architecting for Scale and Safety

Module 1: Why TypeScript?

From Dynamic Freedom to Static Safety

The Problem with JavaScript at Scale

JavaScript's dynamic typing is a double-edged sword. It's fast for prototyping, but dangerous for large, complex backend applications.

What happens when you refactor a function?


                        function calculateTotal(items) {
                            // What if `items` is not an array?
                            // What if an item doesn't have a `price` property?
                            return items.reduce((acc, item) => acc + item.price, 0);
                        }

                        calculateTotal( { price: 100 } ); // 💣 TypeError: items.reduce is not a function
                    

This error would only be discovered at runtime, potentially crashing your production server.

TypeScript: A Superset of JavaScript

TypeScript is JavaScript, with an added layer: a static type system.

  • All valid JavaScript code is valid TypeScript code.
  • TypeScript adds syntax for defining the "shape" of your data and functions.
  • A TypeScript compiler (tsc) checks your code for type errors before you run it, and then compiles it down to plain JavaScript that Node.js can execute.

It turns runtime errors into compile-time errors.

The Same Problem, Solved.

With TypeScript, the error is caught instantly in your code editor.


                        interface Item {
                            price: number;
                        }

                        function calculateTotal(items: Item[]) {
                            return items.reduce((acc, item) => acc + item.price, 0);
                        }

                        // 🚨 ERROR! Argument of type '{ price: number; }'
                        // is not assignable to parameter of type 'Item[]'.
                        calculateTotal( { price: 100 } );
                     

This is the core value proposition: safety, predictability, and maintainability for large-scale applications.

Module 2: TypeScript Fundamentals

Defining the Shape of Your Data

Basic Types

You can explicitly annotate variables with their type.


                        let username: string = 'Alex';
                        let port: number = 3000;
                        let isRunning: boolean = true;
                        
                        // Arrays can be typed in two ways:
                        let roles: string[] = ['admin', 'editor'];
                        let userIds: Array = [1, 2, 3];

                        // `any` is an escape hatch. It opts out of type checking.
                        // AVOID USING THIS WHENEVER POSSIBLE!
                        let data: any = 'could be anything';
                    

Type Inference

TypeScript is smart. If you declare and initialize a variable at the same time, it will infer the type for you.


                        // No need to write `: string`. TS knows `port` is a number.
                        let port = 3000;

                        // port = '3000'; // 🚨 ERROR! Type 'string' is not assignable to type 'number'.
                    

Best practice is to rely on type inference when possible to keep your code clean.

Custom Types: interface

An `interface` is a way to define the shape of an object.


                        interface User {
                            id: number;
                            name: string;
                            email: string;
                            isActive: boolean;
                            role?: string; // The `?` makes this property optional
                        }

                        function printUser(user: User) {
                            console.log(`User ${user.name} (${user.email})`);
                        }

                        const myUser: User = {
                            id: 1,
                            name: 'Alex',
                            email: 'alex@example.com',
                            isActive: true
                        };

                        printUser(myUser); // Works!
                     

Custom Types: type

The `type` keyword can also define object shapes, but it's more versatile. It's particularly useful for creating Union Types.

A union type allows a variable to be one of several types.


                        type Status = 'pending' | 'shipped' | 'delivered' | 'cancelled';
                        type UserId = string | number;

                        let currentStatus: Status = 'pending';
                        // currentStatus = 'processing'; // 🚨 ERROR!

                        let id: UserId = 123;
                        id = 'abc-456'; // This is also valid.
                      

Generics

Generics allow you to write reusable components that can work over a variety of types rather than a single one.

Think of them as variables for types.


                        // A generic type for a standard API response
                        interface ApiResponse {
                            status: 'success' | 'error';
                            data: T; // The data can be of any type `T`
                            message?: string;
                        }

                        // We can now create specific response types
                        const userResponse: ApiResponse = {
                            status: 'success',
                            data: { id: 1, name: 'Alex', ... }
                        };

                        const productsResponse: ApiResponse = {
                            status: 'success',
                            data: [ { id: 101, name: 'Laptop' }, ... ]
                        };
                    

Mid-Lecture Knowledge Check

Module 3: Converting Express to TypeScript

Architecting a Type-Safe Server

Step 1: Project Setup

We need to install TypeScript and some helper tools.


                        # Install TypeScript and ts-node as dev dependencies
                        npm install -D typescript ts-node

                        # Install type definition files for Node and Express
                        npm install -D @types/node @types/express
                    
  • `ts-node`: A tool that lets you run TypeScript files directly without compiling them first. Perfect for development.
  • `@types/...`: Express and Node are written in JavaScript. These are community-maintained "type declaration" files that tell TypeScript the shapes of all the objects and functions from those libraries.

Step 2: The `tsconfig.json` File

This file is the blueprint for your TypeScript project. It tells the compiler how to behave.


                        # This command creates a default tsconfig.json file
                        npx tsc --init
                    

Key options to set:


                        {
                          "compilerOptions": {
                            "target": "ES2020",      // Compile to a modern JS version
                            "module": "CommonJS",    // The module system to use
                            "outDir": "./dist",      // Where to put compiled JS files
                            "rootDir": "./src",      // Where our TS source files are
                            "strict": true,          // Enable all strict type-checking options
                            "esModuleInterop": true
                          }
                        }
                     

Step 3: Update `package.json` Scripts

We update our scripts to use `ts-node` for development and `tsc` for a production build.


                        "scripts": {
                          "start": "node dist/app.js",
                          "build": "tsc",
                          "dev": "nodemon src/app.ts"
                        }
                     

Now, `npm run dev` will run our app with `ts-node`, and `npm run build` will compile our TypeScript to JavaScript in the `dist` folder.

Step 4: Typing the Application

Now we rename our `.js` files to `.ts` and start adding types!

`src/app.ts`


                        import express, { Request, Response, NextFunction } from 'express';
                        // Use ES Module `import` syntax now!

                        const app = express();
                        const PORT: number = 3000;

                        app.get('/', (req: Request, res: Response) => {
                            res.send('Hello from TypeScript!');
                        });

                        app.listen(PORT, () => {
                            console.log(`Server running on port ${PORT}`);
                        });
                    

Notice we import types like `Request`, `Response`, and `NextFunction` directly from `express`.

Typing a Controller

Let's convert our `getUserById` controller.


                        import { Request, Response } from 'express';
                        import { User } from '../entities/User'; // Assuming we created an interface

                        // Type the request parameters
                        interface GetUserRequest extends Request {
                            params: {
                                id: string;
                            }
                        }

                        export const getUserById = (req: GetUserRequest, res: Response) => {
                            const id = parseInt(req.params.id);
                            
                            // ... logic to find user ...
                            const user: User | undefined = users.find(u => u.id === id);

                            if (!user) {
                                return res.status(404).send('User not found');
                            }
                            res.json(user);
                        };
                    

In-Class Practical Exercise

Converting the `createUser` Endpoint

You will be given the complete JavaScript Express API from last week. Your task is to set up the TypeScript environment and convert the `createUser` controller and route to be fully type-safe.

  1. Initialize the project with `npm install`.
  2. Install `typescript`, `ts-node`, `@types/node`, and `@types/express` as dev dependencies.
  3. Create a `tsconfig.json` file using `npx tsc --init` and configure it.
  4. Rename `app.js` and your user-related files to `.ts`.
  5. Create an `interface User` that defines the shape of a user object.
  6. In your `userController.ts`, create a new `interface CreateUserRequest` that properly types the `req.body`.
    interface CreateUserRequest extends Request {
      body: { name: string; email: string; };
    }
  7. Update the `createUser` function signature to use your new `CreateUserRequest` type.
  8. Fix any type errors the TypeScript compiler finds.
  9. Run the server using `ts-node` to verify your work.

Final Knowledge Check