🧬Advanced Types

Unlock the real power of TypeScript with advanced constructs that enable flexible, powerful type expressions.

🛠️ Union, Intersection, and Literal Types

Welcome to Union, Intersection, and Literal Types in TypeScript! These advanced type concepts will help you create more flexible and safe APIs. Let's explore each of them in detail.

Union Types: Flexibility at Its Best

A union type allows a variable to be one of several types. This is incredibly useful when you need flexibility in your API endpoints or data structures.

function greeting(message: string | number) {
  if (typeof message === 'string') {
    return `Hello, ${message}!`;
  }
  return `Hello, Number ${message}!`;
}

greeting('World'); // 'Hello, World!'
 greeting(42); // 'Hello, Number 42!'

💡 Key Points About Union Types

  • Use unions when you need to accept multiple types of input
  • Always use type guards (like typeof or instanceof) for safe type checking
  • Avoid overly broad unions (e.g., string | any)
  • NestJS uses union types extensively in API request/response schemas

Example: API Response with Union Types

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

interface Error {
  code: string;
  message: string;
}

function fetchResource(id: number): Promise<User | Error> {
  // API implementation
}

💡 Best Practices for Union Types

  • Keep unions specific and focused
  • Use never type to prevent unintended cases
  • Leverage exhaustive checks in TypeScript 4.6+
  • Document union types clearly for maintainability

Intersection Types: Combining Strengths

An intersection type combines multiple interfaces into one, enforcing all their properties. This is powerful for creating highly specific types.

interface Square {
  side: number;
}

interface Colored {
  color: string;
}

const square: Square & Colored = {
  side: 5,
  color: 'red'
};

💡 Key Points About Intersection Types

  • Use intersections when you need to combine multiple interfaces
  • Intersection types are often used in NestJS for complex DTOs
  • Be cautious with deep intersections that create overly specific types
  • Always test type compatibility thoroughly

Example: Intersection Types in DTOs

interface BaseDTO {
  id: number;
  createdAt: Date;
}

interface User extends BaseDTO {
  username: string;
  email: string;
}

const createUser = (data: User): Promise<User & { success: boolean }> => {
  // Implementation
};

Literal Types: Precision Matters

A literal type restricts a value to a specific literal. This is perfect for creating safe enums or validation schemas.

type Status = 'active' | 'inactive';

function updateStatus(status: Status) {
  // API implementation
}

updateStatus('active'); // Valid
updateStatus('pending'); // Error

💡 Key Points About Literal Types

  • Use literals for strict validation of allowed values
  • Combine with unions and intersections for complex type schemas
  • NestJS uses literal types extensively in validation decorators
  • Always document literal types clearly for maintainability

Example: Literal Types in API Validation

interface CreateOrderDTO {
  status: 'pending' | 'confirmed';
  type: 'delivery' | 'pickup';
  amount: number;
}

const order: CreateOrderDTO = {
  status: 'pending', // Valid
  type: 'delivery', // Valid
  amount: 100.50
};

💡 Best Practices for Literal Types

  • Use literals for allowed values only
  • Combine with unions for flexible yet safe schemas
  • Avoid overly broad literals that reduce type safety
  • Always test literal types thoroughly in your API

Tagged Unions and Discriminated Types

A tagged union uses a common property (like type or kind) to distinguish between different variants. This enables safe pattern matching in TypeScript.

interface Animal {
  type: 'cat' | 'dog';
  name: string;
}

const animals: Animal[] = [
  { type: 'cat', name: 'Whiskers' },
  { type: 'dog', name: 'Buddy' }
];

💡 Key Points About Tagged Unions

  • Use a discriminant property (like type or kind) to distinguish variants
  • Tagged unions enable safe pattern matching in TypeScript
  • NestJS uses tagged unions extensively in API request/response schemas
  • Always include exhaustive checks to prevent errors

Example: Tagged Union in API Response

interface SuccessResponse {
  status: 'success';
  data: any;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

function handleResponse(response: SuccessResponse | ErrorResponse) {
  if (response.status === 'success') {
    return processSuccess(response);
  }
  return handleError(response);
}

💡 Best Practices for Tagged Unions

  • Use a discriminant property for clear type distinction
  • Include exhaustive checks to prevent missing cases
  • Document all possible variants clearly in your API
  • Combine with other TypeScript features like never type and as const assertions

💡 Real-World Applications of Advanced Types

Advanced types are essential in modern NestJS applications. They enable: - Safe API endpoints with strict type validation - Flexible DTOs for complex data structures - Pattern matching and exhaustive checks - Type-safe enums and validation schemas

🧪 Type Guards and Narrowing

Welcome to Type Guards and Narrowing! In this chapter, we'll explore how to use TypeScript's powerful type system to ensure precise control flow analysis. You'll learn about built-in type guards like typeof⌖, instanceof⌖, and ⌘in⌖ checks, as well as creating custom type guards to handle complex scenarios.

💡 What are Type Guards?

Type guards are mechanisms that allow you to °narrow⌖ the type of a variable within a specific scope. This is especially useful when working with union types, as it enables TypeScript to understand exactly which type a variable represents at a particular point in your code.

  • Type guards ensure type safety while maintaining flexibility.
  • They help prevent runtime errors by enforcing strict typing during development.
  • Common type guards include typeof⌖, instanceof⌖, and custom helper functions.

💡 Built-in Type Guards

TypeScript provides several built-in type guards that you can use to check the type of a variable.

  • typeof: Checks the type of primitives (e.g., string, number, boolean)
  • instanceof: Checks if an object is an instance of a specific class
  • in: Verifies property existence on an object
// Example using typeof
function getType(value: unknown) {
  if (typeof value === 'string') {
    return 'This is a string';
  }
  if (typeof value === 'number') {
    return 'This is a number';
  }
  return 'Unknown type';
}

💡 Custom Type Guards

For more complex scenarios, you can create custom type guards using helper functions or classes.

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// Usage
function getStringLength(value: unknown) {
  if (isString(value)) {
    return value.length;
  }
  throw new Error('Value is not a string');
}

💡 Practical Example: User Authentication

interface User {
  type: 'user';
  id: string;
  name: string;
}

interface Guest {
  type: 'guest';
  sessionId: string;
}

function getUserInfo(userOrGuest: User | Guest) {
  if (userOrGuest.type === 'user') {
    return `User: ${userOrGuest.name}`;
  }
  return `Guest session ID: ${userOrGuest.sessionId}`;
}

Common Mistakes to Avoid

  • Avoid using ⌘any⌖ when type guards can provide better control.
  • Don't overuse type assertions as they bypass TypeScript's safety checks.
  • Be cautious with ⌘in° checks; ensure properties exist before accessing them.

Best Practices for Type Guards

  • Use built-in type guards whenever possible.
  • Create descriptive custom type guards with clear intent.
  • Combine multiple checks for complex scenarios.

💡 Real-World Applications

Type guards are essential in applications where data validation and type safety are critical, such as: - Web APIs handling different request types - User interfaces managing various component states - Data processing pipelines ensuring consistent input formats

💡 Key Takeaways

  • Type guards enable precise control flow analysis with union types.
  • Built-in type guards provide basic functionality; custom guards handle complex cases.
  • Always prioritize type safety and maintainability in your TypeScript code.

🌀 Conditional and Mapped Types

Welcome to the world of Conditional and Mapped Types! These powerful tools allow you to create dynamic, reusable type logic that can adapt to different scenarios. Using extends, keyof, and in, you can transform types in sophisticated ways.

💡 Key Principles of Conditional & Mapped Types

  • Conditional Types: Create type logic that branches based on conditions
  • Mapped Types: Transform existing types by mapping over their properties
  • Use keyof to access property names of a type
  • Leverage in for checking if a type extends another

Let's explore these concepts through practical examples.

Conditional Types Deep Dive

Conditional types allow you to create type logic that changes based on conditions. The basic syntax is: Type1 extends Type2 ? ResultIfTrue : ResultIfFalse.

// Example of a conditional type

type ConditionalExample<T> = T extends string
  ? string // If T is a string, use string
  : number; // Otherwise, use number

Here are some common patterns:

  • Check if a type has a specific property: T extends { prop: any } ? ... : ...
  • Determine if a value is a string or number: T extends string ? ... : ...
  • Create conditional interfaces based on type extensions

💡 Understanding keyof and in Operators

The keyof operator returns the union of keys in a type. The in operator checks if one type extends another.

// Example of keyof

type Keys = keyof { name: string; age: number }; // 'name' | 'age'

// Example of in

type IsString<T> = T extends string ? true : false;

These operators are especially useful for creating generic utilities.

Mastering Mapped Types

Mapped types allow you to transform existing types by mapping over their properties. The syntax is similar to array.map(), but for types.

// Example of a mapped type

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

Common use cases include:

  • Making properties optional: Partial<T>
  • Extracting a subset of properties: Pick<T, K>
  • Omitting certain properties: Omit<T, K>
  • Creating read-only versions of types

These techniques are essential for building flexible and reusable type definitions.

💡 Best Practices for Using Conditional & Mapped Types

  • Always start with a clear use case before creating custom types
  • Use extends for conditional checks whenever possible
  • Leverage built-in utilities like Pick and Omit before writing custom mapped types
  • Write descriptive type names that explain their purpose
  • Test your types thoroughly in different scenarios

🧱 Template Literal Types

Welcome to Template Literal Types in TypeScript! In this chapter, you'll learn how to create type-safe strings using template literals. These advanced types allow you to dynamically construct string patterns with precision and safety.

💡 What are Template Literal Types?

Template literal types are a feature in TypeScript that allows you to define string types using template literals. These types can include static parts and dynamic parts, providing precise control over the structure of your strings.

const filePath: `src/${string}.ts` = 'src/example.ts';

💡 Key Features of Template Literal Types

  • Support for static string parts using template literals
  • Ability to define dynamic segments with specific types
  • Automatic validation of string patterns during compilation
  • Integration with other TypeScript features like union types and generics

Creating Type-Safe Strings

Here's how to create a simple template literal type:

// Define a file path pattern
const filePath: `src/${string}.ts` = 'src/example.ts';

// TypeScript will validate the pattern at compile-time
const invalidPath: `src/${string}.ts` = 'example.txt'; // Error!

Use template literal types to enforce specific string patterns, such as API endpoints or CSS class names.

// Define an API endpoint pattern
const apiEndpoint: `api/v${number}/${string}` = 'api/v1/users';

// Only valid numbers and paths are allowed
const invalidEndpoint: `api/v${number}/${string}` = 'api/vx/users'; // Error!

💡 Real-World Applications

  • File paths: `src/${string}.ts`
  • CSS class names: `.grid-${string}-columns`
  • API endpoints: `/api/v${number}/${string}`
  • URL parameters: `/users/${string}/posts`

Best Practices

  • Use template literals for complex string patterns that require validation
  • Keep template literal types simple and focused on their purpose
  • Combine with other TypeScript features like generics and interfaces
  • Document your template literal types clearly in comments

Common Pitfalls

  • Avoid using template literals for simple strings where type safety isn't critical
  • Don't mix static and dynamic parts without proper validation
  • Don't overcomplicate your templates - keep them maintainable

Quiz

Question 1 of 19

What is a §union type§ in TypeScript?

  • A type that can be one of several types
  • A type that combines multiple interfaces
  • A literal value restriction
  • A type that extends another type