# NodeJs : Advanced TypeScript for Backend

Let's break down advanced TypeScript for backend development. Mastering these concepts is what distinguishes a senior developer, as it allows you to build systems that are not just functional but also robust, scalable, and less prone to bugs.

---

### **Part 1: The Three Pillars of Advanced Types**

At the core of advanced TypeScript are three concepts that allow you to manipulate and create types programmatically. Understanding them deeply is non-negotiable for an 8-year-experience level developer.

#### **1\. Generics: Writing Reusable, Type-Safe Code**

**Core Concept:** Generics are like function arguments but for types. They allow you to write a function or a class that can work with any type, while still maintaining full type safety for that specific type.

Think of a simple function that returns what you pass in. Without generics, you'd use `any`, losing all type information:

TypeScript

```javascript
// Bad: Loses type information
function identity(arg: any): any {
  return arg;
}
```

With generics, you create a type variable (commonly `T` for Type) that captures the type of the input and uses it for the output.

TypeScript

```javascript
// Good: Preserves type information
function identity<T>(arg: T): T {
  return arg;
}

const num = identity(10); // num is inferred as type 'number'
const str = identity("hello"); // str is inferred as type 'string'
```

**Advanced Backend Application:** A common use case is creating a standardized API response structure. You want the structure to be consistent, but the `data` payload will change for each endpoint.

TypeScript

```javascript
// A generic wrapper for all our API responses
interface ApiResponse<T> {
  success: boolean;
  statusCode: number;
  data: T;
  error?: string;
}

function createSuccessResponse<T>(data: T): ApiResponse<T> {
  return {
    success: true,
    statusCode: 200,
    data: data,
  };
}

// Usage for a user endpoint
interface User {
  id: string;
  name: string;
}
const userResponse = createSuccessResponse<User>({ id: '123', name: 'Alex' });
// userResponse.data.name is strongly typed!

// Usage for a product endpoint
interface Product {
  sku: string;
  price: number;
}
const productResponse = createSuccessResponse<Product>({ sku: 'abc', price: 99.99 });
// productResponse.data.price is strongly typed!
```

---

#### **2\. Mapped Types: Transforming Existing Types**

**Core Concept:** Mapped types let you create new types by iterating over the properties of an existing type (`in keyof T`). This is incredibly powerful for creating variations of your data models without repeating code. The built-in `Partial<T>`, `Readonly<T>`, and `Required<T>` are all mapped types.

Let's look at how `Partial<T>` works under the hood:

TypeScript

```javascript
// Makes all properties of T optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};
```

**Advanced Backend Application:** In a backend, you often need different "shapes" of the same data model. For example, when creating a user, the `id` and `createdAt` fields are not provided. When updating, all fields might be optional. Mapped types are perfect for this.

Let's create a custom mapped type that makes all properties of an object writable, which is useful when working with objects returned from a database that might be typed as `readonly`.

TypeScript

```javascript
type Mutable<T> = {
  -readonly [P in keyof T]: T[P]; // The '-' removes the 'readonly' modifier
};

// Imagine our ORM returns a readonly user
interface ReadonlyUser {
  readonly id: string;
  readonly name: string;
  readonly email: string;
}

// We can create a mutable version for manipulation
type WritableUser = Mutable<ReadonlyUser>;
// Now you can modify properties on an object of type WritableUser
```

---

#### **3\. Conditional Types: Type-Level** `if/else`

**Core Concept:** Conditional types allow you to choose a type based on a condition. They follow the structure `T extends U ? X : Y`, which means "if `T` is assignable to `U`, then the type is `X`, otherwise it's `Y`".

They are often combined with the `infer` keyword, which lets you "extract" a type from within another type during the condition check.

**Advanced Backend Application:** A common task is to know the type that a function returns. Or, more complexly, if a function returns a `Promise`, you want to get the type the promise resolves to. This is essential for creating robust service layers.

Let's build a utility type `UnwrapPromise<T>`:

TypeScript

```javascript
// If T is a Promise of some type R, then the type is R. Otherwise, it's just T.
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;

// --- Example Usage ---

// A function that returns a user object directly
function getUserSync(id: string): User {
  return { id, name: 'Alex' };
}

// A function that returns a user object within a promise
async function getUserAsync(id: string): Promise<User> {
  return { id, name: 'Alex' };
}

// Let's get the return types
type SyncUser = UnwrapPromise<ReturnType<typeof getUserSync>>; // Type is User
type AsyncUser = UnwrapPromise<ReturnType<typeof getUserAsync>>; // Type is also User!

// Now you can use this type 'AsyncUser' in other parts of your system,
// knowing you have the actual data model type, regardless of how it was fetched.
```

---

### **Part 2: The Generic Repository Pattern**

This task combines generics and other advanced types to create a highly reusable and type-safe database abstraction layer. This pattern is heavily used in frameworks like NestJS with TypeORM or Prisma.

**Goal:** Create a single `Repository` class that can handle CRUD for any data model (`User`, `Product`, `Order`) without rewriting the logic.

TypeScript

```javascript
// 1. Define a base interface that all our models must have.
interface BaseEntity {
  id: string | number;
}

// 2. Define our data models.
interface User extends BaseEntity {
  id: string;
  name: string;
  email: string;
}

interface Product extends BaseEntity {
  id: number;
  sku: string;
  price: number;
}

// 3. The Generic Repository Class
// T is a generic type parameter that MUST extend our BaseEntity
class GenericRepository<T extends BaseEntity> {
  // In a real app, you would pass a database model/table reference here
  // e.g., constructor(private model: SomeOrmModel<T>) {}

  // The 'create' method should not accept an 'id'
  async create(data: Omit<T, 'id'>): Promise<T> {
    console.log('Creating a new record with data:', data);
    // Real implementation: await this.model.create(data);
    const newRecord = { id: 'new-id-from-db', ...data } as T;
    return newRecord;
  }

  async findById(id: T['id']): Promise<T | null> {
    console.log(`Finding record with id: ${id}`);
    // Real implementation: await this.model.findById(id);
    return null; // Placeholder
  }

  // The 'update' data should be partial
  async update(id: T['id'], data: Partial<Omit<T, 'id'>>): Promise<T | null> {
    console.log(`Updating record ${id} with:`, data);
    // Real implementation: await this.model.update(id, data);
    return null; // Placeholder
  }

  async delete(id: T['id']): Promise<boolean> {
    console.log(`Deleting record with id: ${id}`);
    // Real implementation: await this.model.delete(id);
    return true;
  }
}

// --- How to use it ---
const userRepository = new GenericRepository<User>();
userRepository.create({ name: 'Bob', email: 'bob@example.com' }); // Type-safe! 'id' is not allowed.
userRepository.update('some-user-id', { name: 'Robert' }); // Type-safe! Only user fields are allowed.

const productRepository = new GenericRepository<Product>();
productRepository.create({ sku: 'TSHIRT-RED', price: 25.00 });
// productRepository.update(123, { name: 'New Name' }); // ERROR! 'name' is not a property of Product.
```

---

### **Part 3: Compile-Time vs. Runtime Safety (Your Question)**

> "How can you use TypeScript to ensure both compile-time and runtime type safety for incoming API requests?"

This is a critical point that trips up many developers. **TypeScript types are erased when compiled to JavaScript.** This means `interface User { ... }` provides zero protection at runtime. An API client can send a `POST` request with a completely invalid body, and your code will break if you assume the data shape is correct.

The industry-standard solution is to use a runtime validation library that can infer TypeScript types. **Zod is the leader here.**

The workflow is:

1. **Define a Schema:** You define the shape and rules of your data using Zod. This is your "single source of truth".
    
2. **Infer the Type:** You use Zod's `infer` feature to automatically generate a static TypeScript type from the schema.
    
3. **Validate at Runtime:** In your controller or API handler, you use the schema to `parse` (validate) the incoming request body.
    

This gives you the best of both worlds:

* **Compile-time safety:** The inferred type is used throughout your application, so TypeScript will catch errors if you try to access `user.emial` instead of [`user.email`](http://user.email).
    
* **Runtime safety:** Zod's `parse` method ensures that any data coming from the outside world (e.g., an API request) strictly conforms to your schema before your business logic runs.
    

**Example (NestJS/Express Style):**

TypeScript

```javascript
import { z } from 'zod';

// 1. Define the SCHEMA (the single source of truth)
const createUserSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters long"),
  email: z.string().email(),
  age: z.number().positive().optional(),
});

// 2. INFER the TypeScript type from the schema
type CreateUserDto = z.infer<typeof createUserSchema>;
// This is equivalent to:
// type CreateUserDto = {
//   name: string;
//   email: string;
//   age?: number | undefined;
// }

// --- In your Controller/Service ---

// `body` is typed as CreateUserDto for COMPILE-TIME safety.
// Your IDE will give you autocomplete for `body.name`, `body.email`, etc.
function createUser(body: CreateUserDto) {
  // Business logic here...
  console.log(`Creating user: ${body.name}`);
}

// --- In your API request handler (the entry point) ---

// req.body comes from the client and is unsafe (of type 'any')
function handleCreateUserRequest(req: any, res: any) {
  try {
    // 3. VALIDATE the raw, unsafe data at RUNTIME
    const validatedBody = createUserSchema.parse(req.body);

    // If validation passes, `validatedBody` is guaranteed to have the shape of CreateUserDto.
    // We can now safely pass it to our business logic.
    createUser(validatedBody);

    res.status(201).send("User created");
  } catch (error) {
    // If validation fails, Zod throws a detailed error.
    res.status(400).send(error);
  }
}
```
