TypeScript Conditional Types

This chapter explores TypeScript conditional types, a powerful feature for performing type logic based on conditions. Conditional types enable developers to create dynamic and flexible types that adapt to varying inputs and use cases, providing enhanced type safety and maintainability.

Chapter Goal

  • To understand what conditional types are and their purpose in TypeScript.
  • To learn how to create and use conditional types effectively.
  • To explore practical applications of conditional types in real-world scenarios.

Key Characteristics for TypeScript Conditional Types

  • Type Logic: Performs checks and applies different types based on conditions.
  • Flexibility: Adapts dynamically to varying inputs.
  • Key Syntax: Uses T extends U ? X : Y to define conditions.
  • Combination with Utility Types: Works seamlessly with mapped types and utility types.
  • Dynamic Decision Making: Creates adaptive and reusable type definitions.

Basic Rules for TypeScript Conditional Types

  1. Use the extends keyword to define the condition.
  2. Return one type if the condition is true and another if false.
  3. Combine conditional types with utility types for advanced transformations.
  4. Test conditional types with diverse inputs to ensure correctness.
  5. Avoid overly complex conditional logic for readability.

Best Practices

  1. Use conditional types to handle varying type scenarios dynamically.
  2. Combine conditional types with mapped and utility types for enhanced functionality.
  3. Document complex conditional types for clarity and maintainability.
  4. Limit nested conditional logic to avoid reducing code readability.
  5. Test conditional types thoroughly with edge cases to ensure correctness.

Syntax Table

Serial No Component Syntax Example Description
1 Basic Conditional Type type IsString<T> = T extends string ? true : false; Checks if T is a string.
2 Nested Conditional Type type DeepCheck<T> = T extends Array<infer U> ? U extends string ? true : false : never; Checks nested conditions for arrays.
3 Default Type with Condition type WithDefault<T> = T extends undefined ? string : T; Assigns a default type when T is undefined.
4 Combining with Mapped Types `type Nullable = { [P in keyof T]: T[P] null };` Adds nullability to each property of T.
5 Infer Keyword Usage type ElementType<T> = T extends Array<infer U> ? U : never; Extracts element type from an array.

Syntax Explanation

1. Basic Conditional Type

What is a Basic Conditional Type

A type construct in TypeScript that dynamically evaluates a condition and determines the output type based on whether the condition evaluates to true or false. This mechanism allows for flexible and adaptive type definitions tailored to specific scenarios.

Syntax

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

Detailed Explanation

  • The extends keyword checks if T extends string.
  • Returns true if T is a string; otherwise, returns false.

Example

type Result1 = IsString<string>; // true

type Result2 = IsString<number>; // false

Output

true

false

Notes

  • Use basic conditional types for simple type checks.

Warnings

  • Avoid overloading conditional types with unnecessary logic.

2. Nested Conditional Type

What is a Nested Conditional Type

A TypeScript construct that evaluates multiple conditions hierarchically, enabling developers to define types dynamically based on complex, nested criteria. This approach is especially useful for refining types in layered or deeply structured scenarios.

Syntax

type DeepCheck<T> = T extends Array<infer U> ? U extends string ? true : false : never;

Detailed Explanation

  • The T extends Array<infer U> checks if T is an array and extracts its element type (U).
  • The U extends string checks if the array elements are strings.

Example

type Check1 = DeepCheck<string[]>; // true

type Check2 = DeepCheck<number[]>; // false

Output

true

false

Notes

  • Use nested conditional types for complex type hierarchies.

Warnings

  • Keep nested logic readable and well-documented.

3. Default Type with Condition

What is a Default Type with Condition

A TypeScript construct that dynamically assigns a fallback type when a specified condition is satisfied, ensuring robust type handling in scenarios where a primary type might be undefined or missing.

Syntax

type WithDefault<T> = T extends undefined ? string : T;

Detailed Explanation

  • The T extends undefined checks if T is undefined.
  • Returns string if true; otherwise, returns T.

Example

type Result1 = WithDefault<undefined>; // string

type Result2 = WithDefault<number>; // number

Output

string

number

Notes

  • Use default types to handle missing or undefined values.

Warnings

  • Ensure default types align with the intended use case.

5. Infer Keyword Usage

What is the Infer Keyword

A specialized TypeScript construct that allows extracting and assigning a specific type from a conditional expression, enabling more dynamic and precise type inference in complex scenarios.

Syntax

type ElementType<T> = T extends Array<infer U> ? U : never;

Detailed Explanation

  • The T extends Array<infer U> checks if T is an array.
  • The infer U extracts the element type of the array.

Example

type Type1 = ElementType<string[]>; // string

type Type2 = ElementType<number[]>; // number

Output

string

number

Notes

  • Use infer to extract types dynamically from complex structures.

Warnings

  • Ensure extracted types align with the expected structure.

Real-Life Project

Project Name

Dynamic Data Validator

Project Goal

Demonstrates how to use conditional types to validate and process data dynamically based on its type.

Code for This Project

interface Validator<T> {

  validate: (input: T) => boolean;

}




type CreateValidator<T> = T extends string

  ? { type: 'string'; validate: (input: string) => boolean; }

  : T extends number

  ? { type: 'number'; validate: (input: number) => boolean; }

  : never;




const stringValidator: CreateValidator<string> = {

  type: 'string',

  validate: (input) => input.length > 0,

};




const numberValidator: CreateValidator<number> = {

  type: 'number',

  validate: (input) => input > 0,

};




console.log(stringValidator.validate('Hello')); // Output: true

console.log(numberValidator.validate(-5)); // Output: false

Save and Run

  1. Save the code in your development environment.
  2. Compile the TypeScript code using tsc.
  3. Run the resulting JavaScript file using node.

Expected Output

true

false

Insights

  • Conditional types enable dynamic and flexible type logic.
  • Combining conditional types with mapped types enhances their versatility.
  • Real-world examples like dynamic validators demonstrate their utility.

Key Takeaways

  • Conditional types provide powerful tools for dynamic type definitions.
  • Combine with utility and mapped types for advanced use cases.
  • Test conditional types thoroughly to ensure they handle diverse scenarios.

TypeScript Mapped Types

This chapter focuses on TypeScript mapped types, a powerful feature that allows developers to create new types by transforming existing ones. Mapped types are particularly useful for automating repetitive type transformations, ensuring consistency, and improving code maintainability.

Chapter Goal

  • To understand what mapped types are and their purpose in TypeScript.
  • To learn how to create and use mapped types effectively.
  • To explore practical applications of mapped types in real-world scenarios.

Key Characteristics for TypeScript Mapped Types

  • Type Transformation: Enables creation of new types based on existing ones.
  • Flexibility: Applies modifications to all or selected properties of a type.
  • Built-in Utility Types: Leverages predefined utility types like Partial, Readonly, and Pick.
  • Dynamic Keys: Supports key iteration for dynamic type generation.
  • Customizability: Allows developers to define tailored mapped types for specific needs.

Basic Rules for TypeScript Mapped Types

  1. Use mapped types to iterate over keys of an existing type.
  2. Apply transformations using syntax like [Key in keyof T]: Type.
  3. Combine mapped types with conditional types for advanced logic.
  4. Leverage built-in utility types for common transformations.
  5. Avoid overcomplicating mapped types to maintain readability.

Best Practices

  1. Use meaningful names for mapped type definitions.
  2. Combine mapped types with conditional types for advanced scenarios.
  3. Document custom mapped types for better maintainability.
  4. Prefer built-in utility types for standard transformations.
  5. Test mapped types with diverse inputs to ensure correctness.

Syntax Table

Serial No Component Syntax Example Description
1 Basic Mapped Type type Readonly<T> = { [P in keyof T]: T[P] }; Makes all properties of T readonly.
2 Partial Mapped Type type Partial<T> = { [P in keyof T]?: T[P] }; Makes all properties of T optional.
3 Pick Utility Type type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; Selects a subset of properties from T.
4 Record Utility Type type Record<K extends keyof any, T> = { [P in K]: T }; Creates a type with keys of type K and values of type T.

Syntax Explanation

1. Basic Mapped Type

What is a Basic Mapped Type

A method for iterating over each key in an existing type to systematically apply a transformation, such as modifying properties, changing value types, or imposing constraints, to create a new derived type.

Syntax

type Readonly<T> = {

  [P in keyof T]: T[P];

};

Detailed Explanation

  • The keyof T retrieves all keys of the type T.
  • The [P in keyof T] iterates over each key, applying a transformation.
  • This example creates a type with identical properties to T.

Example

interface User {

  id: number;

  name: string;

}

 

type ReadonlyUser = Readonly<User>;

 

const user: ReadonlyUser = { id: 1, name: ‘Alice’ };

// user.id = 2; // Error: Cannot assign to ‘id’ because it is a read-only property.

Output

// Error: Cannot assign to ‘id’

Notes

  • Use mapped types for consistent transformations across properties.

Warnings

  • Avoid over-transforming types, which can lead to complex and unreadable code.

2. Partial Mapped Type

What is a Partial Mapped Type

A special mapped type in TypeScript that modifies all properties of a given type to make them optional, allowing flexibility when working with partially complete objects.

Syntax

type Partial<T> = {

  [P in keyof T]?: T[P];

};

Detailed Explanation

  • The ? modifier makes each property optional.
  • Useful for scenarios like initializing objects incrementally.

Example

interface User {

  id: number;

  name: string;

}

 

type PartialUser = Partial<User>;

 

const user: PartialUser = { id: 1 };

console.log(user); // Output: { id: 1 }

Output

{ id: 1 }

Notes

  • Use Partial when only some properties of a type need to be defined.

Warnings

  • Ensure optional properties are handled correctly in the consuming code.

3. Pick Utility Type

What is the Pick Utility Type

A utility type in TypeScript designed to create a new type by extracting a subset of properties from an existing type, offering flexibility and precision in type definitions for specific use cases.

Syntax

type Pick<T, K extends keyof T> = {

  [P in K]: T[P];

};

Detailed Explanation

  • The K extends keyof T ensures K is a subset of keys from T.
  • The [P in K] selects only the specified keys.

Example

interface User {

  id: number;

  name: string;

  email: string;

}

 

type UserPreview = Pick<User, ‘id’ | ‘name’>;

 

const user: UserPreview = { id: 1, name: ‘Alice’ };

console.log(user); // Output: { id: 1, name: ‘Alice’ }

Output

{ id: 1, name: ‘Alice’ }

Notes

  • Use Pick to create focused types for specific use cases.

Warnings

  • Avoid overusing Pick, as it may lead to fragmented type definitions.

4. Record Utility Type

What is the Record Utility Type

A utility type in TypeScript used to define a new type where the keys are specified and the values share a common type. This is particularly useful for scenarios requiring consistent mappings, such as permissions, configurations, or dynamic objects.

Syntax

type Record<K extends keyof any, T> = {

  [P in K]: T;

};

Detailed Explanation

  • The K represents the set of keys, and T represents the value type.
  • Creates a consistent structure where all keys share the same value type.

Example

type Permissions = ‘read’ | ‘write’ | ‘delete’;

 

type RolePermissions = Record<Permissions, boolean>;

 

const admin: RolePermissions = {

  read: true,

  write: true,

  delete: true,

};

 

console.log(admin); // Output: { read: true, write: true, delete: true }

Output

{ read: true, write: true, delete: true }

Notes

  • Use Record to define consistent mappings between keys and values.

Warnings

  • Ensure keys and values are correctly aligned to avoid runtime errors.

Real-Life Project

Project Name

Dynamic Form Builder

Project Goal

Demonstrates how to use mapped types to create dynamic form configurations with optional and nullable fields.

Code for This Project

interface FormFields {

  username: string;

  password: string;

  rememberMe: boolean;

}




type FormConfig<T> = {

  [P in keyof T]: {

    label: string;

    value: T[P];

    optional?: boolean;

  };

};




const formConfig: FormConfig<FormFields> = {

  username: { label: 'Username', value: 'JohnDoe' },

  password: { label: 'Password', value: '********' },

  rememberMe: { label: 'Remember Me', value: true, optional: true },

};




console.log(formConfig);

Save and Run

  1. Save the code into files with appropriate names, such as formConfig.ts for the example above.
  2. Open your preferred IDE, such as Visual Studio Code, and ensure that TypeScript is properly configured.
  3. Compile the TypeScript file into JavaScript using the command tsc formConfig.ts in your terminal.
  4. Locate the output JavaScript file in the same directory, typically named formConfig.js.
  5. Run the JavaScript file using Node.js with the command node formConfig.js.
  6. Verify the output in the terminal to ensure it matches the expected result.
  7. Save the code in your development environment.
  8. Compile the TypeScript code into JavaScript using tsc.
  9. Run the resulting JavaScript file using node.

Expected Output

{

  username: { label: ‘Username’, value: ‘JohnDoe’ },

  password: { label: ‘Password’, value: ‘********’ },

  rememberMe: { label: ‘Remember Me’, value: true, optional: true }

}

Insights

  • Mapped types simplify repetitive type transformations, improving maintainability.
  • Built-in utility types like Partial and Readonly provide robust solutions for common scenarios.
  • Real-world examples like dynamic form builders demonstrate their practical utility.

Key Takeaways

  • Mapped types enable efficient type transformations in TypeScript.
  • Combine mapped types with utility types and conditional types for advanced use cases.
  • Test mapped types in diverse scenarios to ensure reliability and correctness.

TypeScript Decorators

This chapter explores TypeScript decorators, a powerful feature for enhancing and modifying classes, methods, properties, or parameters at runtime. Decorators provide a declarative way to implement cross-cutting concerns like logging, validation, or dependency injection.

Chapter Goal

  • To understand what decorators are and how they work in TypeScript.
  • To learn how to create and use different types of decorators.
  • To explore practical applications of decorators in TypeScript projects.

Key Characteristics for TypeScript Decorators

  • Metadata Annotation: Decorators attach metadata to classes or class members.
  • Runtime Execution: Executed at runtime to modify behavior or add functionality.
  • Extensibility: Enable reusable and configurable behavior across code.
  • Support for Angular: Widely used in frameworks like Angular for dependency injection and metadata.
  • Types: Include class, method, accessor, property, and parameter decorators.

Basic Rules for TypeScript Decorators

  1. Use the @ symbol followed by the decorator name.
  2. Ensure experimentalDecorators is enabled in the tsconfig.json file.
  3. Apply decorators to classes, methods, properties, or parameters.
  4. Decorators can be stacked and executed in a specific order.
  5. Combine decorators with metadata reflection for advanced use cases.

Best Practices

  1. Use decorators to encapsulate reusable behaviors.
  2. Document custom decorators for better readability and maintainability.
  3. Avoid overusing decorators to maintain code simplicity.
  4. Combine decorators with dependency injection for cleaner architecture.
  5. Test decorator behavior thoroughly to ensure reliability.

Syntax Table

Serial No Component Syntax Example Description
1 Class Decorator @Component Enhances class functionality or metadata.
2 Method Decorator @Log Modifies method behavior or tracks execution.
3 Property Decorator @Inject Attaches metadata to a property for dependency injection.
4 Accessor Decorator @Validate Applies validation logic to a getter or setter.
5 Parameter Decorator @Param Adds metadata to method parameters.

Syntax Explanation

1. Class Decorator

What is a Class Decorator

A decorator in TypeScript applied to a class to attach metadata, enhance its capabilities, or modify its behavior at runtime. This is often used to add reusable properties or functionalities to the class, making it an essential tool for frameworks like Angular.

Syntax

function Component(target: Function): void {

  target.prototype.componentName = target.name;

}

 

@Component

class AppComponent {}

Detailed Explanation

  • The Component decorator adds a componentName property to the class prototype.
  • Used widely in frameworks like Angular for defining components.

Example

@Component

class AppComponent {}

 

console.log(new AppComponent().componentName); // Output: AppComponent

Output

AppComponent

Notes

  • Use class decorators to apply cross-cutting concerns to entire classes.

Warnings

  • Ensure decorators are used appropriately to avoid unexpected side effects.

2. Method Decorator

What is a Method Decorator

A decorator in TypeScript used to wrap, enhance, or intercept the behavior of a method at runtime. Method decorators allow developers to add additional functionality such as logging, input validation, or performance monitoring while maintaining the core method logic.

Syntax

function Log(target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {

  const originalMethod = descriptor.value;

 

  descriptor.value = function (…args: any[]) {

    console.log(`Method ${propertyKey} called with args:`, args);

    return originalMethod.apply(this, args);

  };

}

 

class Calculator {

  @Log

  add(a: number, b: number): number {

    return a + b;

  }

}

Detailed Explanation

  • The Log decorator wraps the add method, logging arguments before execution.
  • Enhances the method without modifying its core logic.

Example

const calculator = new Calculator();

console.log(calculator.add(2, 3)); // Logs: Method add called with args: [2, 3] and Output: 5

Output

Method add called with args: [2, 3]

5

Notes

  • Use method decorators for logging, validation, or performance tracking.

Warnings

  • Avoid modifying the original method’s logic in unexpected ways.

3. Property Decorator

What is a Property Decorator

A decorator in TypeScript used to attach metadata or alter the behavior of a property at runtime. This is often employed in scenarios like dependency injection, logging, or defining configurations for frameworks.

Syntax

function Inject(target: Object, propertyKey: string): void {

  console.log(`Injected into property: ${propertyKey}`);

}

 

class Service {

  @Inject

  apiUrl: string;

}

Detailed Explanation

  • The Inject decorator adds metadata to the apiUrl property.
  • Commonly used in dependency injection frameworks.

Example

const service = new Service();

// Logs: Injected into property: apiUrl

Output

Injected into property: apiUrl

Notes

  • Use property decorators for injecting dependencies or setting metadata.

Warnings

  • Ensure that injected dependencies are resolved correctly to avoid runtime errors.

4. Accessor Decorator

What is an Accessor Decorator

A specialized decorator in TypeScript used on getters or setters to enforce specific rules, validate input values, or modify their runtime behavior. This helps ensure data integrity and allows for centralized control over property access.

Syntax

function Validate(target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {

  const originalSetter = descriptor.set;

 

  descriptor.set = function (value: any) {

    if (value < 0) {

      throw new Error(‘Value must be non-negative’);

    }

    originalSetter!.call(this, value);

  };

}

 

class Account {

  private _balance: number = 0;

 

  @Validate

  set balance(amount: number) {

    this._balance = amount;

  }

 

  get balance(): number {

    return this._balance;

  }

}

Detailed Explanation

  • The Validate decorator ensures the balance value is non-negative.
  • Useful for enforcing validation rules on properties.

Example

const account = new Account();

account.balance = 100;

console.log(account.balance); // Output: 100

// account.balance = -50; // Throws Error: Value must be non-negative

Output

100

Notes

  • Use accessor decorators to apply constraints or validations.

Warnings

  • Ensure validation logic is comprehensive to cover all edge cases.

5. Parameter Decorator

What is a Parameter Decorator

A TypeScript feature that allows attaching additional metadata or behavior to a method parameter. This can be used to enable advanced scenarios like runtime validation, logging, or dependency injection, ensuring enhanced control and insight over method execution.

Syntax

function Param(target: Object, propertyKey: string, parameterIndex: number): void {

  console.log(`Parameter index ${parameterIndex} in method ${propertyKey}`);

}

 

class Greeter {

  greet(@Param message: string): void {

    console.log(message);

  }

}

Detailed Explanation

  • The Param decorator logs the index of the decorated parameter.
  • Adds metadata to parameters for advanced use cases like validation or logging.

Example

const greeter = new Greeter();

greeter.greet(‘Hello!’); // Logs: Parameter index 0 in method greet and Output: Hello!

Output

Parameter index 0 in method greet

Hello!

Notes

  • Use parameter decorators for logging or metadata collection.

Warnings

  • Avoid overcomplicating parameter logic, which can reduce readability.

Real-Life Project

Project Name

API Request Logger

Project Goal

Demonstrates how to use decorators for logging API requests in a service class.

Code for This Project

function LogRequest(target: Object, propertyKey: string, descriptor: PropertyDescriptor): void {

  const originalMethod = descriptor.value;




  descriptor.value = async function (...args: any[]) {

    console.log(`Request to API with args:`, args);

    const result = await originalMethod.apply(this, args);

    console.log(`Response from API:`, result);

    return result;

  };

}




class ApiService {

  @LogRequest

  async fetchData(endpoint: string): Promise<any> {

    // Simulate API call

    return { data: `Response from ${endpoint}` };

  }

}




const apiService = new ApiService();

apiService.fetchData('/users');

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code.
  2. Compile the TypeScript code into JavaScript using tsc.
  3. Run the resulting JavaScript file using node.

Expected Output

Request to API with args: [‘/users’]

Response from API: { data: ‘Response from /users’ }

Insights

  • Decorators streamline the implementation of cross-cutting concerns like logging and validation.
  • Combining multiple decorators enables modular and reusable functionality.
  • Real-world examples like API logging highlight their practical benefits.

Key Takeaways

  • Decorators enhance and modify behavior in a declarative way.
  • Use decorators responsibly to maintain code readability and simplicity.
  • Test decorators thoroughly to ensure they work as intended in all scenarios.

TypeScript Modules

This chapter introduces TypeScript modules, a powerful mechanism for organizing and managing code. Modules allow developers to break down applications into reusable and maintainable units, promoting modular programming and better code management.

Chapter Goal

  • To understand what TypeScript modules are and their purpose.
  • To learn how to create, import, and export modules.
  • To explore practical applications of modules in TypeScript projects.

Key Characteristics for TypeScript Modules

  • Code Encapsulation: Encapsulates logic into separate files for clarity and reuse.
  • Named and Default Exports: Supports multiple ways to export and import.
  • Dependency Management: Resolves dependencies between different parts of the code.
  • Isolation: Avoids name collisions by maintaining a local scope for each module.
  • Compatibility: Integrates seamlessly with JavaScript and modern module systems.

Basic Rules for TypeScript Modules

  1. Use export to expose functions, classes, or variables from a module.
  2. Use import to include functions, classes, or variables from another module.
  3. Always include a module resolution strategy (e.g., ES Modules or CommonJS).
  4. Avoid circular dependencies between modules.
  5. Group related functionalities into a single module for better organization.

Best Practices

  1. Use meaningful file and module names to indicate their purpose.
  2. Prefer named exports for better flexibility and clarity.
  3. Organize modules into directories for large projects.
  4. Document the functionality of modules to improve maintainability.
  5. Test modules independently before integrating them into the main application.

Syntax Table

Serial No Component Syntax Example Description
1 Named Export export const add = (a: number, b: number) => a + b; Exports a named function or variable.
2 Default Export export default function greet() {} Exports a default function or class.
3 Import Named Export import { add } from ‘./math’; Imports a specific named export from a module.
4 Import Default Export import greet from ‘./greetings’; Imports the default export from a module.
5 Wildcard Import import * as Utils from ‘./utilities’; Imports all exports from a module into a namespace.

Syntax Explanation

1. Named Export

What is a Named Export

A mechanism in TypeScript that allows developers to export multiple functions, classes, or variables individually by name. This approach provides flexibility by enabling selective imports and better organization when modules expose multiple functionalities.

Syntax

// math.ts

export const add = (a: number, b: number) => a + b;

export const subtract = (a: number, b: number) => a – b;

Detailed Explanation

  • Exports add and subtract functions by name.
  • Consumers can selectively import the required functions.

Example

// main.ts

import { add, subtract } from ‘./math’;

 

console.log(add(5, 3)); // Output: 8

console.log(subtract(5, 3)); // Output: 2

Output

8

2

Notes

  • Use named exports for functions or variables shared across multiple files.

Warnings

  • Ensure export names do not conflict with imports in the consuming module.

2. Default Export

What is a Default Export

A mechanism in TypeScript that enables the export of a single function, class, or object as the primary feature of a module. This simplifies the import process by allowing consumers to access the module’s main functionality without needing to specify a name in curly braces.

Syntax

// greetings.ts

export default function greet(): void {

  console.log(‘Hello, World!’);

}

Detailed Explanation

  • The default export does not require curly braces during import.
  • Each module can have only one default export.

Example

// main.ts

import greet from ‘./greetings’;

 

greet(); // Output: Hello, World!

Output

Hello, World!

Notes

  • Use default exports for modules exposing a single functionality.

Warnings

  • Avoid mixing default and named exports in the same module for simplicity.

3. Import Named Export

What is Import Named Export

A mechanism in TypeScript that allows developers to import only the specific functionalities they need from a module. This approach reduces unnecessary imports, optimizes performance by enabling tree-shaking, and improves clarity by explicitly stating what is being used.

Syntax

import { add } from ‘./math’;

Detailed Explanation

  • Allows importing only the required functionalities from a module.
  • Helps reduce the size of the imported code when tree-shaking is enabled.

Example

// main.ts

import { add } from ‘./math’;

 

console.log(add(10, 20)); // Output: 30

Output

30

Notes

  • Use named imports to avoid importing unnecessary code.

Warnings

  • Ensure the import statement matches the exact export name.

4. Import Default Export

What is Import Default Export

A concise and straightforward syntax in TypeScript used to import the primary functionality of a module that has been designated as the default export. This approach simplifies usage by omitting the need for curly braces and allows developers to focus on the main purpose of the module.

Syntax

import greet from ‘./greetings’;

Detailed Explanation

  • No curly braces are needed when importing the default export.
  • Each module can have only one default export, simplifying its usage.

Example

// main.ts

import greet from ‘./greetings’;

 

greet(); // Output: Hello, World!

Output

Hello, World!

Notes

  • Use default imports for modules exposing one primary functionality.

Warnings

  • Avoid using ambiguous names for default imports to maintain clarity.

5. Wildcard Import

What is a Wildcard Import

A flexible syntax in TypeScript that allows developers to group all exports from a module under a single namespace. This approach simplifies access to multiple functionalities within the module, providing a clear and organized way to use utility functions or related features without importing them individually.

Syntax

import * as Utils from ‘./utilities’;

Detailed Explanation

  • Groups all exports under a single namespace (e.g., Utils).
  • Useful for importing utility modules with multiple functions.

Example

// utilities.ts

export const add = (a: number, b: number) => a + b;

export const subtract = (a: number, b: number) => a – b;

 

// main.ts

import * as Utils from ‘./utilities’;

 

console.log(Utils.add(4, 2)); // Output: 6

console.log(Utils.subtract(4, 2)); // Output: 2

Output

6

2

Notes

  • Use wildcard imports for utility or helper modules.

Warnings

  • Avoid wildcard imports in modules with too many exports to reduce namespace clutter.

Real-Life Project

Project Name

E-Commerce Utilities

Project Goal

Demonstrates how to use modules for organizing reusable utility functions in an e-commerce application.

Code for This Project

// priceCalculator.ts

export function calculateDiscount(price: number, discount: number): number {

  return price - price * (discount / 100);

}




export function calculateTax(price: number, taxRate: number): number {

  return price + price * (taxRate / 100);

}




// main.ts

import { calculateDiscount, calculateTax } from './priceCalculator';




const discountedPrice = calculateDiscount(100, 10);

const finalPrice = calculateTax(discountedPrice, 5);




console.log(`Discounted Price: $${discountedPrice}`); // Output: Discounted Price: $90

console.log(`Final Price with Tax: $${finalPrice}`); // Output: Final Price with Tax: $94.5

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the files are named priceCalculator.ts and main.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc.
  3. Execute the resulting JavaScript file using the command node main.js.

Expected Output

Discounted Price: $90

Final Price with Tax: $94.5

Insights

  • Modules enhance code organization and reusability by grouping related functionalities.
  • Named and default exports provide flexibility in how modules are consumed.
  • Real-world applications like e-commerce utilities highlight the practical utility of modules.

Key Takeaways

  • TypeScript modules encapsulate code, making it reusable and maintainable.
  • Use named exports for multiple functionalities and default exports for single-purpose modules.
  • Organize large projects into modular structures for better scalability and maintainability.

TypeScript Type Guards

This chapter explores TypeScript type guards, a feature that allows developers to refine types during runtime. By enabling conditional type checks, type guards provide enhanced type safety and flexibility in complex codebases.

Chapter Goal

  • To understand what type guards are and their purpose in TypeScript.
  • To learn how to define and use type guards effectively.
  • To explore practical applications of type guards in real-world scenarios.

Key Characteristics for TypeScript Type Guards

  • Runtime Type Refinement: Allows narrowing down types at runtime.
  • Conditional Logic: Combines type checks with logical conditions for better clarity.
  • Type Safety: Ensures safer operations by validating types before use.
  • Customizable: Supports custom implementations using user-defined functions.
  • Integration with Control Flow: Works seamlessly with TypeScript’s type inference.

Basic Rules for TypeScript Type Guards

  1. Use built-in type guards like typeof and instanceof for basic checks.
  2. Implement user-defined type guards for custom logic.
  3. Combine type guards with conditional statements for runtime type narrowing.
  4. Avoid overusing type guards, as they can make the code harder to maintain.
  5. Use type guards with union or intersection types for more precise handling.

Best Practices

  1. Use type guards to handle uncertain or mixed types in complex code.
  2. Prefer built-in type guards for primitive types and standard objects.
  3. Document user-defined type guards for better readability.
  4. Combine type guards with exhaustive checks for union types.
  5. Test custom type guards thoroughly to ensure correctness.

Syntax Table

Serial No Component Syntax Example Description
1 typeof Guard if (typeof value === ‘string’) {} Checks primitive types like string, number.
2 instanceof Guard if (value instanceof ClassName) {} Checks object instances against a class.
3 User-Defined Guard function isString(value: any): value is string {} Custom type guard for specific logic.
4 Discriminated Union if (shape.kind === ‘circle’) {} Narrows down union types based on unique properties.
5 in Operator Guard if (‘property’ in obj) {} Checks for property existence in an object.

Syntax Explanation

1. typeof Guard

What is a typeof Guard

A built-in type guard that verifies the type of a primitive value at runtime, such as string, number, or boolean. This ensures operations are performed safely by confirming the expected type before execution.

Syntax

if (typeof value === ‘string’) {

  console.log(‘Value is a string.’);

}

Detailed Explanation

  • typeof is used to check primitive types like string, number, boolean, and symbol.
  • Ensures safe operations based on the detected type.

Example

function processValue(value: string | number): void {

  if (typeof value === ‘string’) {

    console.log(value.toUpperCase());

  } else {

    console.log(value * 2);

  }

}

 

processValue(‘hello’); // Output: HELLO

processValue(10); // Output: 20

Output

HELLO

20

Notes

  • Use typeof for checking primitive types only.

Warnings

  • Avoid using typeof for complex objects, as it returns “object” for arrays and null.

2. instanceof Guard

What is an instanceof Guard

A built-in type guard that checks if an object belongs to a specific class or inherits from it. This allows developers to verify the prototype chain of the object and ensures safe access to class-specific properties and methods.

Syntax

if (value instanceof ClassName) {

  console.log(‘Value is an instance of ClassName.’);

}

Detailed Explanation

  • instanceof is used to validate the class of an object.
  • Ensures safe access to properties or methods defined in the class.

Example

class Animal {}

class Dog extends Animal {

  bark(): void {

    console.log(‘Woof!’);

  }

}

 

function handleAnimal(animal: Animal): void {

  if (animal instanceof Dog) {

    animal.bark();

  } else {

    console.log(‘Not a dog.’);

  }

}

 

handleAnimal(new Dog()); // Output: Woof!

handleAnimal(new Animal()); // Output: Not a dog.

Output

Woof!

Not a dog.

Notes

  • Use instanceof for class-based type checks.

Warnings

  • Ensure that the constructor function or class is accessible for the instanceof check.

3. User-Defined Guard

What is a User-Defined Guard

A custom function in TypeScript that enables developers to validate and refine types based on specific conditions or logic. These functions return a type predicate (value is Type) that helps the compiler narrow down the type of a value at runtime, ensuring precise type handling in complex scenarios.

Syntax

function isString(value: any): value is string {

  return typeof value === ‘string’;

}

Detailed Explanation

  • User-defined guards return a value is Type predicate to narrow the type.
  • Enables handling of complex or non-standard type checks.

Example

function isArray(value: any): value is Array<any> {

  return Array.isArray(value);

}

 

function processInput(input: string | any[]): void {

  if (isArray(input)) {

    console.log(`Array of length: ${input.length}`);

  } else {

    console.log(`String of length: ${input.length}`);

  }

}

 

processInput(‘Hello’); // Output: String of length: 5

processInput([1, 2, 3]); // Output: Array of length: 3

Output

String of length: 5

Array of length: 3

Notes

  • Use user-defined guards for scenarios not covered by built-in guards.

Warnings

  • Ensure the guard logic is accurate to avoid runtime errors.

4. Discriminated Union

What is a Discriminated Union

A design pattern in TypeScript that utilizes a common, unique property (discriminant) present in all variants of a union type. This property allows the TypeScript compiler to determine the specific type of an object at runtime, enabling precise type refinement and access to relevant properties or methods for each variant.

Syntax

if (shape.kind === ‘circle’) {

  console.log(‘Shape is a circle.’);

}

Detailed Explanation

  • Works with union types that have a common discriminant property.
  • Narrows the type to the specific variant matching the property value.

Example

type Shape =

  | { kind: ‘circle’; radius: number }

  | { kind: ‘square’; side: number };

 

function getArea(shape: Shape): number {

  if (shape.kind === ‘circle’) {

    return Math.PI * shape.radius ** 2;

  } else {

    return shape.side ** 2;

  }

}

 

console.log(getArea({ kind: ‘circle’, radius: 5 })); // Output: 78.53981633974483

console.log(getArea({ kind: ‘square’, side: 4 })); // Output: 16

Output

78.53981633974483

16

Notes

  • Use discriminated unions to simplify handling of related types.

Warnings

  • Ensure all union variants are covered to avoid runtime errors.

5. in Operator Guard

What is an in Operator Guard

A type guard that validates whether a specific property exists within an object at runtime. This ensures safe access to the property and enables narrowing the type of the object based on the presence of the property, facilitating precise and error-free operations in dynamic scenarios.

Syntax

if (‘property’ in obj) {

  console.log(‘Property exists in the object.’);

}

Detailed Explanation

  • Checks whether a property exists on an object at runtime.
  • Narrows down types based on the presence of specific properties.

Example

function processEntity(entity: { name: string } | { age: number }): void {

  if (‘name’ in entity) {

    console.log(`Name: ${entity.name}`);

  } else {

    console.log(`Age: ${entity.age}`);

  }

}

 

processEntity({ name: ‘Alice’ }); // Output: Name: Alice

processEntity({ age: 30 }); // Output: Age: 30

Output

Name: Alice

Age: 30

Notes

  • Use the in operator for property existence checks in dynamic objects.

Warnings

  • Avoid using in for arrays or primitive types.

Real-Life Project

Project Name

Dynamic Entity Processor

Project Goal

Demonstrates how to use type guards to process entities with varying structures safely.

Code for This Project

type Entity = { name: string } | { id: number };




function processEntity(entity: Entity): void {

  if ('name' in entity) {

    console.log(`Entity Name: ${entity.name}`);

  } else {

    console.log(`Entity ID: ${entity.id}`);

  }

}




processEntity({ name: 'Alice' }); // Output: Entity Name: Alice

processEntity({ id: 42 }); // Output: Entity ID: 42

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the file is named entityProcessor.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc entityProcessor.ts in your terminal.
  3. Execute the resulting JavaScript file using the command node entityProcessor.js.

Expected Output

Entity Name: Alice

Entity ID: 42

Insights

  • Type guards improve type safety by refining types dynamically at runtime.
  • Built-in and user-defined guards cater to a wide range of scenarios.
  • Real-world examples like entity processing highlight their practical utility.

Key Takeaways

  • Type guards refine types during runtime, enabling safer and more flexible operations.
  • Use built-in guards like typeof and instanceof for basic checks.
  • Leverage discriminated unions and user-defined guards for more complex scenarios.

TypeScript Type Assertions

This chapter explains type assertions in TypeScript, a technique for explicitly specifying a value’s type. Type assertions help developers inform the TypeScript compiler about the intended type of a value, providing greater flexibility while maintaining type safety.

Chapter Goal

  • To understand what type assertions are and their role in TypeScript.
  • To learn how to use type assertions effectively in various scenarios.
  • To explore practical applications of type assertions for narrowing or overriding types.

Key Characteristics for TypeScript Type Assertions

  • Compiler Hint: Provides additional type information to the TypeScript compiler.
  • Flexible Conversion: Allows treating a value as a specific type.
  • Non-Destructive: Does not modify the runtime behavior of the code.
  • Syntax Variants: Supports as syntax and angle bracket (<>) syntax.
  • Error Prevention: Reduces type errors in specific scenarios.

Basic Rules for TypeScript Type Assertions

  1. Use as or <> syntax to perform a type assertion.
  2. Ensure the asserted type is compatible with the original type.
  3. Avoid asserting incompatible or unsafe types.
  4. Prefer type assertions over any for narrowing types.
  5. Use type assertions sparingly and only when necessary.

Best Practices

  1. Use type assertions to resolve known limitations of TypeScript’s type inference.
  2. Avoid using type assertions to bypass compiler errors.
  3. Combine type assertions with runtime checks for safety.
  4. Document the reason for using type assertions in complex scenarios.
  5. Use as const for literal type preservation.

Syntax Table

Serial No Component Syntax Example Description
1 as Syntax let name = value as string; Common syntax for type assertions.
2 Angle Bracket Syntax let name = <string>value; Alternative syntax for type assertions.
3 DOM Example let input = document.getElementById(‘input’) as HTMLInputElement; Asserts a DOM element’s type.
4 Literal Assertion const obj = { prop: ‘value’ } as const; Preserves literal types.
5 Combining Assertions const item = (value as unknown) as string; Chains type assertions for type narrowing.

Syntax Explanation

1. as Syntax

What is as Syntax

A concise and flexible syntax in TypeScript used to explicitly define the type of a value when the compiler cannot infer it accurately. This approach informs the TypeScript compiler about the intended type, allowing developers to enforce type safety while handling scenarios with uncertain types or complex type relationships.

Syntax

let value: unknown = ‘Hello’;

let name = value as string;

Detailed Explanation

  • The as keyword explicitly informs the compiler to treat value as a string.
  • It provides flexibility when the type cannot be inferred directly.

Example

let data: unknown = ‘TypeScript’;

let length = (data as string).length;

console.log(length); // Output: 10

Output

10

Notes

  • Use as syntax in scenarios requiring type narrowing.

Warnings

  • Avoid asserting unrelated or incompatible types.

2. Angle Bracket Syntax

What is Angle Bracket Syntax

An older but valid syntax for type assertions in TypeScript that uses angle brackets (<>). This syntax is commonly found in legacy code and works similarly to the as syntax. However, it is not compatible with JSX files, where angle brackets are reserved for HTML-like syntax.

Syntax

let value: unknown = ‘Hello’;

let name = <string>value;

Detailed Explanation

  • Similar to as syntax but uses angle brackets (<>).
  • Common in older TypeScript codebases but incompatible with JSX.

Example

let data: unknown = ‘TypeScript’;

let length = (<string>data).length;

console.log(length); // Output: 10

Output

10

Notes

  • Use as syntax in JSX or modern TypeScript projects.

Warnings

  • Avoid angle bracket syntax in JSX files to prevent parsing issues.

3. DOM Example

What is a DOM Example

Using type assertions to explicitly define the type of a DOM element when its exact type cannot be inferred automatically by the TypeScript compiler. This approach ensures that properties and methods specific to the element’s type are available for use, thereby enhancing type safety and enabling proper DOM interactions.

Syntax

let input = document.getElementById(‘input’) as HTMLInputElement;

Detailed Explanation

  • Ensures input is treated as an HTMLInputElement.
  • Enables accessing specific properties like .value.

Example

let input = document.getElementById(‘input’) as HTMLInputElement;

if (input) {

  console.log(input.value);

}

Output

// Value of the input field

Notes

  • Combine with null checks to avoid runtime errors.

Warnings

  • Ensure the DOM element matches the asserted type to prevent unexpected behavior.

4. Literal Assertion

What is a Literal Assertion

Using as const to enforce immutability and ensure that an object or array retains its exact literal types, locking down the structure and values to prevent unintended changes. This is particularly useful for configurations, predefined settings, or constants that must remain unchanged throughout the program.

Syntax

const obj = { prop: ‘value’ } as const;

Detailed Explanation

  • Marks obj as immutable and preserves its literal types.
  • Prevents accidental modification of the obj structure.

Example

const point = { x: 10, y: 20 } as const;

// point.x = 15; // Error: Cannot assign to ‘x’ because it is a read-only property.

Output

// Error: Read-only property

Notes

  • Use as const for constants or configurations.

Warnings

  • Ensure the immutability aligns with the intended behavior.

5. Combining Assertions

What is Combining Assertions

Chaining type assertions is the process of applying multiple assertions in sequence to refine or override a value’s type more effectively. This technique is particularly useful in scenarios where intermediate type transformations are required to achieve the desired type compatibility or clarity, especially when dealing with complex type hierarchies or unknown values.

Syntax

const item = (value as unknown) as string;

Detailed Explanation

  • Useful when narrowing types in complex scenarios.
  • Allows intermediate type assertions for clarity.

Example

let value: any = ‘TypeScript’;

let length = (value as unknown as string).length;

console.log(length); // Output: 10

Output

10

Notes

  • Combine assertions only when necessary to resolve type issues.

Warnings

  • Avoid excessive chaining, as it may obscure code readability.

Real-Life Project

Project Name

DOM Manipulation Helper

Project Goal

Demonstrates how to use type assertions for safe and flexible DOM element interactions.

Code for This Project

function getInputValue(elementId: string): string | null {

  const element = document.getElementById(elementId) as HTMLInputElement;

  return element ? element.value : null;

}




const value = getInputValue('username');

console.log(value); // Output: Value of the input field or null

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the file is named domHelper.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc domHelper.ts in your terminal.
  3. Execute the resulting JavaScript file using the command node domHelper.js.

Expected Output

Value of the input field or null

Insights

  • Type assertions ensure type safety when interacting with DOM elements.
  • They are particularly useful in scenarios where TypeScript cannot infer types directly.
  • Real-world examples like DOM manipulation highlight the practical benefits of type assertions.

Key Takeaways

  • Type assertions allow explicit type specifications, ensuring flexibility and type safety.
  • Use as syntax for modern TypeScript projects and avoid angle brackets in JSX files.
  • Combine assertions with runtime checks for robust and error-free applications.

TypeScript Generics

This chapter delves into generics in TypeScript, a powerful feature that enables developers to create reusable and flexible code components. Generics allow you to write functions, classes, and interfaces that work with a variety of data types while maintaining type safety.

Chapter Goal

  • To understand what generics are and their purpose in TypeScript.
  • To learn how to define and use generics in functions, classes, and interfaces.
  • To explore practical applications of generics in real-world scenarios.

Key Characteristics for TypeScript Generics

  • Type Safety: Ensures type checking while providing flexibility.
  • Reusability: Allows for writing generic functions, classes, and interfaces.
  • Type Parameters: Uses placeholders (like <T>) to represent types.
  • Wide Compatibility: Works with any data type, reducing redundancy.
  • Custom Constraints: Allows restriction of types using extends.

Basic Rules for TypeScript Generics

  1. Use <T> or similar placeholders to define generic types.
  2. Pass specific types when calling generic functions or creating instances.
  3. Use constraints to limit the types that can be passed as generics.
  4. Avoid overcomplicating generics with excessive parameters.
  5. Combine generics with utility types for added flexibility.

Best Practices

  1. Use meaningful names for type parameters (T, K, V, etc.).
  2. Combine generics with constraints to enforce specific type behavior.
  3. Avoid using any in generics to maintain type safety.
  4. Document the purpose of type parameters for better readability.
  5. Leverage built-in utility types (Partial, Readonly, etc.) with generics.

Syntax Table

Serial No Component Syntax Example Description
1 Generic Function function identity<T>(arg: T): T { return arg; } A function that works with any data type.
2 Generic Class class Box<T> { content: T; } A class that holds a value of any type.
3 Generic Interface interface Pair<K, V> { key: K; value: V; } An interface with multiple generic type parameters.
4 Constraints function logLength<T extends { length: number }>(arg: T): void {} Restricts generics to types with a length property.
5 Default Type Parameters function fetch<T = string>(url: string): T {} Assigns a default type to a generic parameter.

Syntax Explanation

1. Generic Function

What is a Generic Function

A function that can operate on arguments of different types, specified when the function is called.

Syntax

function identity<T>(arg: T): T {

  return arg;

}

Detailed Explanation

  • T is a type parameter that acts as a placeholder for the actual type.
  • The identity function accepts an argument of type T and returns a value of the same type.
  • Generics ensure the function maintains type safety while being reusable.

Example

console.log(identity<string>(‘Hello’)); // Output: Hello

console.log(identity<number>(42)); // Output: 42

Output

Hello

42

Notes

  • Use generic functions for operations that apply to various data types.

Warnings

  • Avoid using any in place of generics to retain type safety.

2. Generic Class

What is a Generic Class

A class that can work with properties or methods of different types.

Syntax

class Box<T> {

  content: T;

 

  constructor(value: T) {

    this.content = value;

  }

}

Detailed Explanation

  • T represents a type parameter for the Box class.
  • The content property and constructor parameter use the T type.
  • Allows creating instances of Box for different types.

Example

const stringBox = new Box<string>(‘Books’);

console.log(stringBox.content); // Output: Books

 

const numberBox = new Box<number>(100);

console.log(numberBox.content); // Output: 100

Output

Books

100

Notes

  • Use generic classes for containers or utilities that work with various types.

Warnings

  • Ensure consistent type usage throughout the class to avoid confusion.

3. Generic Interface

What is a Generic Interface

An interface that defines a structure using generic type parameters.

Syntax

interface Pair<K, V> {

  key: K;

  value: V;

}

Detailed Explanation

  • K and V represent key and value types, respectively.
  • Allows defining data structures like key-value pairs with type safety.

Example

const pair: Pair<string, number> = { key: ‘Age’, value: 30 };

console.log(pair.key); // Output: Age

console.log(pair.value); // Output: 30

Output

Age

30

Notes

  • Use generic interfaces for structured data with flexible types.

Warnings

  • Avoid overly complex type parameters in interfaces.

4. Constraints

What are Constraints

Restrictions on generic type parameters to enforce specific type behaviors.

Syntax

function logLength<T extends { length: number }>(arg: T): void {

  console.log(arg.length);

}

Detailed Explanation

  • T extends { length: number } ensures T has a length property.
  • Constraints improve type safety by limiting acceptable types.

Example

logLength(‘Hello’); // Output: 5

logLength([1, 2, 3]); // Output: 3

// logLength(42); // Error: Type ‘number’ does not have a ‘length’ property.

Output

5

3

Notes

  • Use constraints to enforce specific characteristics of generic types.

Warnings

  • Ensure constraints are meaningful and not overly restrictive.

5. Default Type Parameters

What are Default Type Parameters

Generic type parameters with default types when none are explicitly provided.

Syntax

function fetch<T = string>(url: string): T {

  // Implementation

}

Detailed Explanation

  • T = string sets string as the default type for T.
  • If no type is specified, T defaults to string.

Example

function createArray<T = number>(length: number, value: T): T[] {

  return Array(length).fill(value);

}

 

console.log(createArray(3, 42)); // Output: [42, 42, 42]

console.log(createArray(3, ‘Hi’)); // Output: [‘Hi’, ‘Hi’, ‘Hi’]

Output

[42, 42, 42]

[‘Hi’, ‘Hi’, ‘Hi’]

Notes

  • Default type parameters simplify function calls when a common type is expected.

Warnings

  • Avoid using defaults that may conflict with intended use cases.

Real-Life Project

Project Name

Generic Data Processor

Project Goal

Demonstrates how to use generics to create a data processing utility that works with various data types.

Code for This Project

class DataProcessor<T> {

  private data: T[];




  constructor(initialData: T[]) {

    this.data = initialData;

  }




  addItem(item: T): void {

    this.data.push(item);

  }




  getAll(): T[] {

    return this.data;

  }

}




const stringProcessor = new DataProcessor<string>(['Hello']);

stringProcessor.addItem('World');

console.log(stringProcessor.getAll()); // Output: ['Hello', 'World']




const numberProcessor = new DataProcessor<number>([1, 2, 3]);

numberProcessor.addItem(4);

console.log(numberProcessor.getAll()); // Output: [1, 2, 3, 4]

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the file is named dataProcessor.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc dataProcessor.ts in your terminal.
  3. Execute the resulting JavaScript file using the command node dataProcessor.js.

Expected Output

[‘Hello’, ‘World’]

[1, 2, 3, 4]

Insights

  • Generics enhance reusability and type safety for data processing.
  • Combining generics with constraints allows flexible yet controlled code.
  • Real-world examples like data processors highlight the practical benefits of generics.

Key Takeaways

  • Generics provide flexibility and type safety for reusable components.
  • Use constraints to ensure meaningful type interactions.
  • Default type parameters simplify usage in common scenarios.

TypeScript Static Members

This chapter focuses on static members in TypeScript, a feature that allows developers to define properties and methods that belong to the class itself rather than an instance of the class. Static members are particularly useful for utility functions, constants, and shared data.

Chapter Goal

  • To understand what static members are and their role in TypeScript.
  • To learn how to define and use static properties and methods.
  • To explore practical applications of static members in real-world scenarios.

Key Characteristics for TypeScript Static Members

  • Class-Level Association: Static members are accessed via the class name rather than an instance.
  • Shared Across Instances: Static properties and methods are shared among all instances of a class.
  • Utility and Constants: Ideal for defining constants and utility functions.
  • No this Reference: Static methods cannot access instance-specific properties or methods directly.
  • Compile-Time Checks: TypeScript enforces access rules for static members.

Basic Rules for TypeScript Static Members

  1. Use the static keyword to define static properties and methods.
  2. Access static members using the class name, not an instance.
  3. Avoid referencing instance members inside static methods.
  4. Use static properties for shared data or constants.
  5. Use static methods for utility functions that don’t depend on instance data.

Best Practices

  1. Use meaningful names for static members to indicate their shared nature.
  2. Avoid overusing static members in cases where instance-level behavior is appropriate.
  3. Document the purpose of static members to improve code readability.
  4. Combine static methods with namespaces or modules for organization.
  5. Regularly review static members to ensure they are used appropriately.

Syntax Table

Serial No Component Syntax Example Description
1 Static Property static count: number = 0; Declares a static property shared across instances.
2 Static Method static getInstanceCount(): number { return this.count; } Defines a static method accessible via the class.
3 Access Static Members ClassName.member Accesses static properties or methods.
4 Utility Class class MathUtil { static square(x: number): number { return x * x; } } Implements a utility class.
5 Static Initializer static { StaticClass.count = 100; } Initializes static properties.

Syntax Explanation

1. Static Property

What is a Static Property

A property that belongs to the class itself, shared across all instances, and can be accessed directly through the class name. This ensures a single shared value for all objects of the class, making it ideal for global counters, configurations, or shared data that does not vary between instances.

Syntax

class Counter {

  static count: number = 0;

 

  static increment(): void {

    Counter.count++;

  }

}

Detailed Explanation

  • The count property is defined as static, meaning it belongs to the Counter class.
  • Access or modify the count property using the class name, Counter.
  • Static properties are not replicated for individual class instances.

Example

Counter.increment();

console.log(Counter.count); // Output: 1

Counter.increment();

console.log(Counter.count); // Output: 2

Output

1

2

Notes

  • Static properties are ideal for tracking global counts or shared data.

Warnings

  • Avoid excessive reliance on static properties, as it can lead to tightly coupled code.

2. Static Method

What is a Static Method

A method that belongs to the class itself, meaning it is defined at the class level and not tied to any particular instance. Static methods are particularly useful for operations that are not dependent on the state of any instance and can perform tasks like calculations, validations, or utility operations. They are accessed directly using the class name, ensuring clarity and preventing ambiguity with instance-specific methods.

Syntax

class MathUtil {

  static square(x: number): number {

    return x * x;

  }

}

Detailed Explanation

  • Static methods are useful for operations that do not depend on instance-specific data.
  • Call the method using the class name, e.g., MathUtil.square(5).

Example

console.log(MathUtil.square(4)); // Output: 16

console.log(MathUtil.square(7)); // Output: 49

Output

16

49

Notes

  • Use static methods for utility functions, calculations, or validations.

Warnings

  • Static methods cannot access non-static properties or methods directly.

3. Accessing Static Members

How to Access Static Members

Static members are accessed using the class name rather than an instance. This design ensures that static properties and methods are clearly associated with the class itself, promoting consistency and preventing misuse or confusion about their purpose. For example, a static method like ClassName.method() can be invoked without creating an instance, streamlining code for shared functionality or global data.

Syntax

class Logger {

  static logLevel: string = ‘INFO’;

 

  static log(message: string): void {

    console.log(`[${Logger.logLevel}] ${message}`);

  }

}

Detailed Explanation

  • Static properties and methods are accessed directly via the class name.
  • Static members are not available through instances of the class.

Example

Logger.log(‘Application started.’); // Output: [INFO] Application started.

Logger.logLevel = ‘DEBUG’;

Logger.log(‘Debugging…’); // Output: [DEBUG] Debugging…

Output

[INFO] Application started.

[DEBUG] Debugging…

Notes

  • Accessing static members directly via the class name ensures clarity.

Warnings

  • Do not attempt to access static members via instances, as this will result in errors.

4. Utility Class

What is a Utility Class

A class that serves as a container for related static methods and properties, providing a centralized way to organize functionality that doesn’t depend on specific instances. Utility classes streamline the implementation of reusable logic, making them ideal for calculations, validations, or other tasks that are widely applicable across the application.

Syntax

class MathUtil {

  static square(x: number): number {

    return x * x;

  }

 

  static cube(x: number): number {

    return x * x * x;

  }

}

Detailed Explanation

  • Utility classes typically contain static methods that perform common tasks or calculations.
  • These classes do not require instantiation.

Example

console.log(MathUtil.square(3)); // Output: 9

console.log(MathUtil.cube(2)); // Output: 8

Output

9

8

Notes

  • Utility classes help organize and reuse code efficiently.

Warnings

  • Avoid adding instance-specific behavior to utility classes.

5. Static Initializer

What is a Static Initializer

A block of code that initializes static properties at the time the class is first loaded into memory. This ensures that all static properties are properly set up before they are accessed, allowing for complex initialization logic, such as setting default values or loading configuration settings dynamically.

Syntax

class Config {

  static readonly appName: string;

  static readonly version: string;

 

  static {

    Config.appName = ‘MyApp’;

    Config.version = ‘1.0.0’;

  }

}

Detailed Explanation

  • The static initializer block runs once when the class is first loaded.
  • It is useful for setting up complex static properties.

Example

console.log(Config.appName); // Output: MyApp

console.log(Config.version); // Output: 1.0.0

Output

MyApp

1.0.0

Notes

  • Use static initializers for initializing properties that require complex logic.

Warnings

  • Ensure static initializers do not have side effects or depend on external inputs.

Real-Life Project

Project Name

Global Application Settings

Project Goal

Demonstrates how to use static members to define global application settings that are accessible throughout the codebase.

Code for This Project

class AppSettings {

  static readonly appName: string = 'GlobalApp';

  static version: string = '1.0.0';

  static environment: string = 'development';




  static updateEnvironment(env: string): void {

    AppSettings.environment = env;

  }

}




console.log(AppSettings.appName); // Output: GlobalApp

console.log(AppSettings.version); // Output: 1.0.0

console.log(AppSettings.environment); // Output: development




AppSettings.updateEnvironment('production');

console.log(AppSettings.environment); // Output: production

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the file is named appSettings.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc appSettings.ts in your terminal.
  3. Execute the resulting JavaScript file using the command node appSettings.js.

Expected Output

GlobalApp

1.0.0

development

production

Insights

  • Static members provide a centralized way to manage shared data and functions.
  • They are ideal for global configurations, constants, and utility functions.
  • Real-world scenarios like application settings showcase their practical utility.

Key Takeaways

  • Static members belong to the class itself, not to individual instances.
  • Use static properties for shared data and constants.
  • Use static methods for reusable logic that doesn’t depend on instance-specific data.

TypeScript Static Members

This chapter focuses on static members in TypeScript, a feature that allows developers to define properties and methods that belong to the class itself rather than an instance of the class. Static members are particularly useful for utility functions, constants, and shared data.

Chapter Goal

  • To understand what static members are and their role in TypeScript.
  • To learn how to define and use static properties and methods.
  • To explore practical applications of static members in real-world scenarios.

Key Characteristics for TypeScript Static Members

  • Class-Level Association: Static members are accessed via the class name rather than an instance.
  • Shared Across Instances: Static properties and methods are shared among all instances of a class.
  • Utility and Constants: Ideal for defining constants and utility functions.
  • No this Reference: Static methods cannot access instance-specific properties or methods directly.
  • Compile-Time Checks: TypeScript enforces access rules for static members.

Basic Rules for TypeScript Static Members

  1. Use the static keyword to define static properties and methods.
  2. Access static members using the class name, not an instance.
  3. Avoid referencing instance members inside static methods.
  4. Use static properties for shared data or constants.
  5. Use static methods for utility functions that don’t depend on instance data.

Best Practices

  1. Use meaningful names for static members to indicate their shared nature.
  2. Avoid overusing static members in cases where instance-level behavior is appropriate.
  3. Document the purpose of static members to improve code readability.
  4. Combine static methods with namespaces or modules for organization.
  5. Regularly review static members to ensure they are used appropriately.

Syntax Table

Serial No Component Syntax Example Description
1 Static Property static count: number = 0; Declares a static property shared across instances.
2 Static Method static getInstanceCount(): number { return this.count; } Defines a static method accessible via the class.
3 Access Static Members ClassName.member Accesses static properties or methods.
4 Utility Class class MathUtil { static square(x: number): number { return x * x; } } Implements a utility class.
5 Static Initializer static { StaticClass.count = 100; } Initializes static properties.

Syntax Explanation

1. Static Property

What is a Static Property

A property that belongs to the class itself and is shared among all instances.

Syntax

class Counter {

  static count: number = 0;

 

  static increment(): void {

    Counter.count++;

  }

}

Detailed Explanation

  • The count property is defined as static, meaning it belongs to the Counter class.
  • Access or modify the count property using the class name, Counter.
  • Static properties are not replicated for individual class instances.

Example

Counter.increment();

console.log(Counter.count); // Output: 1

Counter.increment();

console.log(Counter.count); // Output: 2

Output

1

2

Notes

  • Static properties are ideal for tracking global counts or shared data.

Warnings

  • Avoid excessive reliance on static properties, as it can lead to tightly coupled code.

2. Static Method

What is a Static Method

A method that belongs to the class itself and can be called without creating an instance.

Syntax

class MathUtil {

  static square(x: number): number {

    return x * x;

  }

}

Detailed Explanation

  • Static methods are useful for operations that do not depend on instance-specific data.
  • Call the method using the class name, e.g., MathUtil.square(5).

Example

console.log(MathUtil.square(4)); // Output: 16

console.log(MathUtil.square(7)); // Output: 49

Output

16

49

Notes

  • Use static methods for utility functions, calculations, or validations.

Warnings

  • Static methods cannot access non-static properties or methods directly.

3. Accessing Static Members

How to Access Static Members

Static members are accessed using the class name, not an instance.

Syntax

class Logger {

  static logLevel: string = ‘INFO’;

 

  static log(message: string): void {

    console.log(`[${Logger.logLevel}] ${message}`);

  }

}

Detailed Explanation

  • Static properties and methods are accessed directly via the class name.
  • Static members are not available through instances of the class.

Example

Logger.log(‘Application started.’); // Output: [INFO] Application started.

Logger.logLevel = ‘DEBUG’;

Logger.log(‘Debugging…’); // Output: [DEBUG] Debugging…

Output

[INFO] Application started.

[DEBUG] Debugging…

Notes

  • Accessing static members directly via the class name ensures clarity.

Warnings

  • Do not attempt to access static members via instances, as this will result in errors.

4. Utility Class

What is a Utility Class

A class that groups related static methods and properties.

Syntax

class MathUtil {

  static square(x: number): number {

    return x * x;

  }

 

  static cube(x: number): number {

    return x * x * x;

  }

}

Detailed Explanation

  • Utility classes typically contain static methods that perform common tasks or calculations.
  • These classes do not require instantiation.

Example

console.log(MathUtil.square(3)); // Output: 9

console.log(MathUtil.cube(2)); // Output: 8

Output

9

8

Notes

  • Utility classes help organize and reuse code efficiently.

Warnings

  • Avoid adding instance-specific behavior to utility classes.

5. Static Initializer

What is a Static Initializer

A block of code that initializes static properties when the class is loaded.

Syntax

class Config {

  static readonly appName: string;

  static readonly version: string;

 

  static {

    Config.appName = ‘MyApp’;

    Config.version = ‘1.0.0’;

  }

}

Detailed Explanation

  • The static initializer block runs once when the class is first loaded.
  • It is useful for setting up complex static properties.

Example

console.log(Config.appName); // Output: MyApp

console.log(Config.version); // Output: 1.0.0

Output

MyApp

1.0.0

Notes

  • Use static initializers for initializing properties that require complex logic.

Warnings

  • Ensure static initializers do not have side effects or depend on external inputs.

Real-Life Project

Project Name

Global Application Settings

Project Goal

Demonstrates how to use static members to define global application settings that are accessible throughout the codebase.

Code for This Project

class AppSettings {

  static readonly appName: string = 'GlobalApp';

  static version: string = '1.0.0';

  static environment: string = 'development';




  static updateEnvironment(env: string): void {

    AppSettings.environment = env;

  }

}




console.log(AppSettings.appName); // Output: GlobalApp

console.log(AppSettings.version); // Output: 1.0.0

console.log(AppSettings.environment); // Output: development




AppSettings.updateEnvironment('production');

console.log(AppSettings.environment); // Output: production

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the file is named appSettings.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc appSettings.ts in your terminal.
  3. Execute the resulting JavaScript file using the command node appSettings.js.

Expected Output

GlobalApp

1.0.0

development

production

Insights

  • Static members provide a centralized way to manage shared data and functions.
  • They are ideal for global configurations, constants, and utility functions.
  • Real-world scenarios like application settings showcase their practical utility.

Key Takeaways

  • Static members belong to the class itself, not to individual instances.
  • Use static properties for shared data and constants.
  • Use static methods for reusable logic that doesn’t depend on instance-specific data.

TypeScript Readonly Modifier

This chapter focuses on the readonly modifier in TypeScript, a feature that restricts properties from being reassigned after initialization. The readonly modifier ensures immutability for class properties and type declarations, enhancing type safety and reducing unintended changes.

Chapter Goal

  • To understand what the readonly modifier is and its purpose in TypeScript.
  • To learn how to use the readonly modifier for properties and types.
  • To explore practical applications of the readonly modifier for enforcing immutability.

Key Characteristics for TypeScript Readonly Modifier

  • Immutability: Ensures that a property cannot be reassigned after it is initialized.
  • Type Safety: Prevents accidental modification of constant values.
  • Declarative Syntax: Simple and clear syntax using the readonly keyword.
  • Class Properties: Often used for defining immutable class members.
  • Interfaces and Types: Applicable to object and array types for immutability.

Basic Rules for TypeScript Readonly Modifier

  1. Use readonly to prevent reassignment of properties after initialization.
  2. Apply readonly to class properties, interfaces, and type declarations.
  3. Initialize readonly properties either inline or in the constructor.
  4. Combine readonly with const for complete immutability.
  5. Avoid using readonly for properties that require updates.

Best Practices

  1. Use readonly for properties that represent constants or fixed values.
  2. Document the purpose of readonly properties to enhance code clarity.
  3. Combine readonly with TypeScript’s utility types like Readonly<T> for deep immutability.
  4. Avoid using readonly for mutable data or frequently updated values.
  5. Regularly review readonly declarations to ensure consistency with the code’s intent.

Syntax Table

Serial No Component Syntax Example Description
1 Basic Readonly Property readonly id: number; Declares a read-only property in a class.
2 Inline Initialization readonly name: string = ‘Alice’; Initializes a readonly property inline.
3 Constructor Assignment readonly balance: number; constructor(balance: number) { this.balance = balance; } Assigns a readonly property in the constructor.
4 Readonly Array readonly numbers: number[] = [1, 2, 3]; Declares a readonly array.
5 Readonly Utility Type Readonly<T> Makes all properties of T read-only.

Syntax Explanation

1. Basic Readonly Property

What is a Basic Readonly Property

A property that cannot be reassigned after it is initialized.

Syntax

class Product {

  readonly id: string;

 

  constructor(id: string) {

    this.id = id;

  }

}

Detailed Explanation

  • The id property is marked as readonly and can only be assigned in the constructor.
  • Attempts to reassign the id property will result in a compile-time error.

Example

const product = new Product(‘P123’);

console.log(product.id); // Output: P123

// product.id = ‘P124’; // Error: Cannot assign to ‘id’ because it is a read-only property.

Output

P123

Notes

  • Use readonly for properties that represent fixed identifiers or constants.

Warnings

  • Ensure that readonly properties are initialized either inline or in the constructor.

2. Inline Initialization

What is Inline Initialization

Assigning a value to a readonly property directly during its declaration.

Syntax

class User {

  readonly name: string = ‘Default User’;

}

Detailed Explanation

  • Inline initialization sets the readonly property at the time of declaration.
  • This approach is useful for properties with default values that rarely change.

Example

const user = new User();

console.log(user.name); // Output: Default User

// user.name = ‘Admin’; // Error: Cannot assign to ‘name’ because it is a read-only property.

Output

Default User

Notes

  • Inline initialization simplifies the class constructor for fixed properties.

Warnings

  • Avoid inline initialization for properties that require dynamic assignment.

3. Constructor Assignment

What is Constructor Assignment

Initializing a readonly property within the class constructor.

Syntax

class Account {

  readonly balance: number;

 

  constructor(balance: number) {

    this.balance = balance;

  }

}

Detailed Explanation

  • Constructor assignment provides flexibility to initialize readonly properties with dynamic values.
  • This approach is commonly used for properties dependent on constructor arguments.

Example

const account = new Account(1000);

console.log(account.balance); // Output: 1000

// account.balance = 2000; // Error: Cannot assign to ‘balance’ because it is a read-only property.

Output

1000

Notes

  • Constructor assignment is ideal for properties initialized with runtime values.

Warnings

  • Ensure that all readonly properties are assigned within the constructor.

4. Readonly Array

What is a Readonly Array

An array declared with the readonly modifier, preventing reassignment of the array reference.

Syntax

class Data {

  readonly values: number[] = [1, 2, 3];

}

Detailed Explanation

  • The array reference is immutable, but the array contents can still be modified.
  • Use TypeScript’s ReadonlyArray<T> for true immutability of array contents.

Example

const data = new Data();

console.log(data.values); // Output: [1, 2, 3]

// data.values = [4, 5, 6]; // Error: Cannot assign to ‘values’ because it is a read-only property.

data.values.push(4); // This is allowed.

console.log(data.values); // Output: [1, 2, 3, 4]

Output

[1, 2, 3]

[1, 2, 3, 4]

Notes

  • Use ReadonlyArray<T> for strict immutability if modifying contents should be restricted.

Warnings

  • Remember that the readonly modifier applies only to the reference, not the array elements.

5. Readonly Utility Type

What is the Readonly Utility Type

A TypeScript utility type that makes all properties of a type read-only.

Syntax

type ReadonlyObject = Readonly<{ name: string; age: number }>;

 

const obj: ReadonlyObject = { name: ‘Alice’, age: 25 };

Detailed Explanation

  • The Readonly utility type applies the readonly modifier to every property of a type.
  • This ensures deep immutability for object structures.

Example

console.log(obj.name); // Output: Alice

// obj.name = ‘Bob’; // Error: Cannot assign to ‘name’ because it is a read-only property.

Output

Alice

Notes

  • Combine Readonly<T> with nested types for comprehensive immutability.

Warnings

  • Avoid overusing Readonly for types that require frequent updates.

Real-Life Project

Project Name

Immutable Configuration Settings

Project Goal

Demonstrates how to use the readonly modifier to define immutable configuration settings for an application.

Code for This Project

class Config {

  readonly appName: string;

  readonly version: string;

  readonly features: ReadonlyArray<string>;




  constructor(appName: string, version: string, features: string[]) {

    this.appName = appName;

    this.version = version;

    this.features = features;

  }

}




const config = new Config('MyApp', '1.0.0', ['login', 'dashboard']);

console.log(config.appName); // Output: MyApp

console.log(config.features); // Output: ['login', 'dashboard']

// config.appName = 'NewApp'; // Error: Cannot assign to 'appName' because it is a read-only property.

// config.features.push('analytics'); // Error if using `ReadonlyArray`.

Save and Run

  1. Save the code in your development environment, such as Visual Studio Code, ensuring the file is named config.ts.
  2. Compile the TypeScript code into JavaScript using the command tsc config.ts in your terminal.
  3. Execute the resulting JavaScript file using the command node config.js.

Expected Output

MyApp

[‘login’, ‘dashboard’]