Skip to content

Instantly share code, notes, and snippets.

@mariogarcia-ar
Last active December 21, 2024 20:52
Show Gist options
  • Save mariogarcia-ar/44bfaceac8f0434b0244660a4bd1f48e to your computer and use it in GitHub Desktop.
Save mariogarcia-ar/44bfaceac8f0434b0244660a4bd1f48e to your computer and use it in GitHub Desktop.
Tutorials for TypeScript

Sure! Here's a basic tutorial on TypeScript to help you get started. πŸš€


Introduction to TypeScript

TypeScript is a superset of JavaScript that adds static typing and other powerful features, making it easier to develop scalable and robust applications.


1. Installation

Prerequisites:

Make sure you have Node.js installed.

Step 1: Install TypeScript

npm install -g typescript

Step 2: Verify Installation

tsc --version

2. Creating Your First TypeScript Program

Step 1: Create a File

Create a file named hello.ts.

Step 2: Add Code

let message: string = "Hello, TypeScript!";
console.log(message);

Step 3: Compile to JavaScript

tsc hello.ts

This generates a hello.js file.

Step 4: Run the JavaScript File

node hello.js

3. Core Concepts in TypeScript

Variables and Types

let isDone: boolean = true;         // Boolean
let age: number = 25;               // Number
let username: string = "John Doe";  // String
let list: number[] = [1, 2, 3];     // Array

Functions with Type Annotations

function greet(name: string): string {
  return `Hello, ${name}!`;
}

console.log(greet("Alice")); // Output: Hello, Alice!

Interfaces

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

let user: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30,
};

console.log(user);

Classes

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a noise.`);
  }
}

let dog = new Animal("Dog");
dog.speak(); // Output: Dog makes a noise.

Generics

function identity<T>(arg: T): T {
  return arg;
}

console.log(identity<number>(5));  // Output: 5
console.log(identity<string>("Hi")); // Output: Hi

4. Compiling and Watching Files

Watch Mode

Automatically recompile files when changes are made:

tsc --watch

5. Configuration with tsconfig.json

Generate a configuration file:

tsc --init

Sample configuration (tsconfig.json):

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "strict": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

6. Running TypeScript with Node.js

  1. Install ts-node (to run TypeScript files directly):
npm install -g ts-node
  1. Execute the TypeScript file:
ts-node hello.ts

7. Advanced Features

Type Assertions

let someValue: any = "This is a string";
let strLength: number = (someValue as string).length;

Union Types

function printId(id: number | string) {
  console.log(`ID: ${id}`);
}

printId(101);        // Output: ID: 101
printId("202");      // Output: ID: 202

Enums

enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

console.log(Direction.Up); // Output: 1

8. Next Steps

  1. Explore TypeScript with React (create-react-app --template typescript).
  2. Build an Express.js API with TypeScript.
  3. Try advanced features like Decorators, Mapped Types, and Utility Types.

πŸ’‘ You're now ready to start coding in TypeScript! Let me know if you'd like to dive deeper into any topic. πŸ”¨πŸ€–πŸ”§

Here's a tutorial for basic types in TypeScript! πŸš€


TypeScript Basic Types Tutorial

TypeScript provides static typing, allowing you to define variable types explicitly. This feature helps catch errors during development. Let's explore the basic types in TypeScript.


1. Boolean

Represents a true or false value.

let isDone: boolean = true; // βœ… Correct
let isPending: boolean = false; // βœ… Correct

// let status: boolean = "true"; ❌ Error: Type 'string' is not assignable to type 'boolean'.

2. Number

Represents both integers and floating-point numbers.

let decimal: number = 6;
let hex: number = 0xf00d;    // Hexadecimal
let binary: number = 0b1010; // Binary
let octal: number = 0o744;   // Octal

// Operations
let sum: number = decimal + 10; // βœ… Allowed

3. String

Represents textual data.

let name: string = "Alice";
let greeting: string = `Hello, ${name}!`; // Template literals
console.log(greeting); // Output: Hello, Alice!

4. Array

Represents a collection of values of the same type.

Syntax 1: Array Type

let numbers: number[] = [1, 2, 3, 4];

Syntax 2: Generic Array Type

let fruits: Array<string> = ["apple", "banana", "mango"];

Readonly Arrays

let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); ❌ Error: Cannot add elements to a readonly array.

5. Tuple

Represents a fixed-size array with known types at each position.

let person: [string, number] = ["Alice", 25];
console.log(person[0]); // Output: Alice
console.log(person[1]); // Output: 25

// person[0] = 30; ❌ Error: Type 'number' is not assignable to type 'string'.

6. Enum

Defines a set of named constants.

enum Color {
  Red,   // 0
  Green, // 1
  Blue,  // 2
}

let c: Color = Color.Green;
console.log(c); // Output: 1

// Custom values
enum Status {
  Success = 200,
  NotFound = 404,
}

console.log(Status.Success); // Output: 200

7. Any

Represents a value that can be of any type. It disables type checking.

let value: any = 42;      // Number
value = "Hello";          // String
value = true;             // Boolean

πŸ’‘ Tip: Avoid using any unless absolutely necessary. It reduces the benefits of TypeScript.


8. Unknown

Similar to any, but safer. Type checks must be performed before using it.

let value: unknown = "Hello";

// value.toUpperCase(); ❌ Error: Object is of type 'unknown'.

if (typeof value === "string") {
  console.log(value.toUpperCase()); // βœ… Safe usage
}

9. Void

Represents functions that do not return a value.

function logMessage(message: string): void {
  console.log(message);
}

logMessage("Logging..."); // Output: Logging...

10. Null and Undefined

Represents null or undefined values.

let u: undefined = undefined;
let n: null = null;

πŸ’‘ Tip: Use strictNullChecks in tsconfig.json to enforce stricter checks.


11. Never

Represents values that never occur, such as errors or infinite loops.

function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

12. Union Types

Represents values that can have multiple types.

let id: number | string; // Can be number or string

id = 101; // βœ…
id = "202"; // βœ…
// id = true; ❌ Error

13. Literal Types

Allows variables to have specific values as types.

let direction: "up" | "down";

direction = "up";   // βœ…
direction = "down"; // βœ…
// direction = "left"; ❌ Error

14. Type Aliases

Define custom types for readability.

type Point = { x: number; y: number };
let position: Point = { x: 10, y: 20 };

15. Optional and Default Values

function greet(name: string, age?: number): string {
  return age ? `Hello ${name}, age ${age}` : `Hello ${name}`;
}

console.log(greet("Alice")); // Output: Hello Alice
console.log(greet("Bob", 30)); // Output: Hello Bob, age 30

Summary Table of Basic Types

Type Example Usage
Boolean let isDone: boolean = true;
Number let age: number = 25;
String let name: string = "Alice";
Array let list: number[] = [1, 2, 3];
Tuple let person: [string, number] = ["Bob", 30];
Enum enum Color { Red, Green, Blue }
Any let value: any = "Hello";
Unknown let value: unknown = 42;
Void function log(): void { console.log("Log"); }
Null let n: null = null;
Undefined let u: undefined = undefined;
Never function error(): never { throw new Error(); }
Union `let id: string
Literal `let dir: "up"

Next Steps

Now that you know the basic types, you can:

  1. Explore Interfaces and Classes in TypeScript.
  2. Learn about Advanced Types like Mapped Types and Conditional Types.
  3. Start building real-world applications using this knowledge!

πŸ’‘ Need more help? Let me know what you'd like to learn next! πŸ”¨πŸ€–πŸ”§

Here’s a tutorial for functions and objects in TypeScript! πŸš€


Functions and Objects in TypeScript

TypeScript provides static typing and type annotations to functions and objects, ensuring better readability and fewer runtime errors. Let’s dive in!


1. Functions in TypeScript

1.1 Function Basics

function add(a: number, b: number): number {
  return a + b;
}

console.log(add(5, 10)); // Output: 15
  • Parameter Types: Enforces that a and b must be numbers.
  • Return Type: Enforces the function to return a number.

1.2 Optional Parameters

Use ? to mark a parameter as optional.

function greet(name: string, age?: number): string {
  if (age !== undefined) {
    return `Hello ${name}, you are ${age} years old.`;
  }
  return `Hello ${name}`;
}

console.log(greet("Alice"));           // Output: Hello Alice
console.log(greet("Bob", 30));         // Output: Hello Bob, you are 30 years old.

1.3 Default Parameters

Provide default values if no arguments are passed.

function greet(name: string = "Guest"): string {
  return `Hello ${name}`;
}

console.log(greet());            // Output: Hello Guest
console.log(greet("Alice"));     // Output: Hello Alice

1.4 Rest Parameters

Accepts an infinite number of arguments as an array.

function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4)); // Output: 10

1.5 Function Type

Define the type signature of a function.

let multiply: (x: number, y: number) => number;

multiply = (x, y) => x * y;

console.log(multiply(3, 4)); // Output: 12

1.6 Anonymous Functions

Type annotations also work with arrow functions.

const square = (x: number): number => x * x;

console.log(square(5)); // Output: 25

1.7 Function Overloading

Define multiple function signatures.

function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: any, b: any): any {
  return a + b;
}

console.log(combine(5, 10));     // Output: 15
console.log(combine("Hello", "World")); // Output: HelloWorld

2. Objects in TypeScript

2.1 Basic Object Types

let person: { name: string; age: number } = {
  name: "Alice",
  age: 25,
};

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

2.2 Optional Properties

let person: { name: string; age?: number } = {
  name: "Bob",
};

console.log(person.age); // Output: undefined

2.3 Readonly Properties

let person: { readonly id: number; name: string } = {
  id: 101,
  name: "Alice",
};

// person.id = 102; ❌ Error: Cannot assign to 'id' because it is a read-only property.

2.4 Nested Objects

let employee: {
  name: string;
  address: {
    city: string;
    country: string;
  };
} = {
  name: "John",
  address: {
    city: "New York",
    country: "USA",
  },
};

console.log(employee.address.city); // Output: New York

2.5 Object with Methods

let car: {
  model: string;
  start(): void;
} = {
  model: "Tesla",
  start() {
    console.log("Car started!");
  },
};

car.start(); // Output: Car started!

3. Interfaces for Objects

3.1 Defining an Interface

interface Person {
  name: string;
  age: number;
  greet(): string;
}

let user: Person = {
  name: "Alice",
  age: 25,
  greet() {
    return "Hello!";
  },
};

console.log(user.greet()); // Output: Hello!

3.2 Optional and Readonly in Interfaces

interface Product {
  readonly id: number;
  name: string;
  price?: number; // Optional
}

let product: Product = {
  id: 1,
  name: "Laptop",
};

// product.id = 2; ❌ Error: Cannot assign to 'id' because it is a read-only property.

3.3 Extending Interfaces

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

let dog: Dog = {
  name: "Buddy",
  breed: "Golden Retriever",
};

console.log(dog.breed); // Output: Golden Retriever

3.4 Index Signatures

Dynamic keys with specific value types.

interface Dictionary {
  [key: string]: string;
}

let translations: Dictionary = {
  hello: "Hola",
  world: "Mundo",
};

console.log(translations.hello); // Output: Hola

4. Combining Functions and Objects

Example: Building a Calculator

interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

let calc: Calculator = {
  add(a, b) {
    return a + b;
  },
  subtract(a, b) {
    return a - b;
  },
};

console.log(calc.add(5, 3));       // Output: 8
console.log(calc.subtract(10, 4)); // Output: 6

5. Type Aliases vs Interfaces

Type Aliases

type Point = { x: number; y: number };
let point: Point = { x: 10, y: 20 };

Interfaces

interface Point {
  x: number;
  y: number;
}
let point: Point = { x: 10, y: 20 };

πŸ’‘ Key Difference:

  • Interfaces can be extended.
  • Type Aliases cannot be reopened for adding new properties after definition.

Summary Table

Feature Example
Basic Function function add(a: number, b: number): number {}
Optional Parameters function greet(name: string, age?: number) {}
Default Parameters function greet(name: string = "Guest") {}
Rest Parameters function sum(...numbers: number[]) {}
Interfaces for Objects interface Person { name: string; age: number; }
Methods in Objects { start() { console.log("Started!"); } }
Readonly Properties { readonly id: number; }
Dynamic Keys interface Dictionary { [key: string]: string; }

Next Steps

  1. Practice Classes and Inheritance in TypeScript.
  2. Learn Generics for more flexible code.
  3. Explore building real-world applications with this foundation.

πŸ’‘ Need help with advanced topics? Let me know! πŸ”¨πŸ€–πŸ”§

Here’s a tutorial for custom functions and custom objects in TypeScript! πŸš€


Custom Functions and Custom Objects in TypeScript

Custom functions and objects allow you to create reusable components with defined structures and behavior. This tutorial covers how to define, use, and extend custom functions and objects effectively in TypeScript.


1. Custom Functions

Custom functions in TypeScript can be designed with specific types, parameters, and return values.


1.1 Function with Custom Types

type AddFunction = (a: number, b: number) => number;

const add: AddFunction = (x, y) => x + y;

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

πŸ’‘ Key Takeaway: Use type aliases to define reusable function types.


1.2 Function with Optional Parameters

function greet(name: string, age?: number): string {
  if (age) {
    return `Hello ${name}, age ${age}`;
  }
  return `Hello ${name}`;
}

console.log(greet("Alice"));         // Output: Hello Alice
console.log(greet("Bob", 25));       // Output: Hello Bob, age 25

1.3 Function with Default Parameters

function greet(name: string = "Guest"): string {
  return `Hello ${name}`;
}

console.log(greet());        // Output: Hello Guest
console.log(greet("Alice")); // Output: Hello Alice

1.4 Rest Parameters

function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4)); // Output: 10

1.5 Function Overloading

function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: any, b: any): any {
  return a + b;
}

console.log(combine(10, 20));       // Output: 30
console.log(combine("Hello ", "World")); // Output: Hello World

πŸ’‘ Key Takeaway: Function overloading allows you to handle different types of input with a single function.


2. Custom Objects

Custom objects in TypeScript are created using interfaces, type aliases, or classes.


2.1 Custom Object with Type Alias

type Person = {
  name: string;
  age: number;
  greet: () => string;
};

const user: Person = {
  name: "Alice",
  age: 25,
  greet() {
    return `Hello, my name is ${this.name}`;
  },
};

console.log(user.greet()); // Output: Hello, my name is Alice

2.2 Custom Object with Interface

interface Product {
  id: number;
  name: string;
  price: number;
  showDetails(): void;
}

const laptop: Product = {
  id: 101,
  name: "Laptop",
  price: 999.99,
  showDetails() {
    console.log(`Product: ${this.name}, Price: $${this.price}`);
  },
};

laptop.showDetails(); // Output: Product: Laptop, Price: $999.99

πŸ’‘ Key Takeaway: Use interfaces to define reusable structures for objects.


2.3 Optional and Readonly Properties

interface Car {
  readonly id: number;  // Can't be changed
  model: string;
  price?: number;       // Optional property
}

const car: Car = {
  id: 1,
  model: "Tesla Model S",
};

// car.id = 2; ❌ Error: Cannot assign to 'id' because it is a read-only property.
console.log(car.model); // Output: Tesla Model S

2.4 Nested Objects

interface Employee {
  name: string;
  position: string;
  address: {
    city: string;
    country: string;
  };
}

const emp: Employee = {
  name: "John",
  position: "Developer",
  address: {
    city: "New York",
    country: "USA",
  },
};

console.log(emp.address.city); // Output: New York

2.5 Dynamic Objects (Index Signatures)

interface Dictionary {
  [key: string]: string; // Dynamic key-value pairs
}

const translations: Dictionary = {
  hello: "Hola",
  world: "Mundo",
};

console.log(translations.hello); // Output: Hola

3. Custom Objects with Classes

Classes in TypeScript allow for blueprints to create objects with methods and properties.


3.1 Basic Class Example

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a noise.`);
  }
}

const dog = new Animal("Dog");
dog.speak(); // Output: Dog makes a noise.

3.2 Inheritance in Classes

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  bark(): void {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy");
dog.speak(); // Output: Buddy makes a noise.
dog.bark();  // Output: Buddy barks.

3.3 Getters and Setters

class Person {
  private _age: number;

  constructor(age: number) {
    this._age = age;
  }

  get age(): number {
    return this._age;
  }

  set age(value: number) {
    if (value > 0) {
      this._age = value;
    } else {
      throw new Error("Age must be positive!");
    }
  }
}

const person = new Person(25);
console.log(person.age); // Output: 25

person.age = 30;
console.log(person.age); // Output: 30

4. Combining Custom Functions and Objects

Example: Managing a Library

interface Book {
  id: number;
  title: string;
  author: string;
}

class Library {
  private books: Book[] = [];

  addBook(book: Book): void {
    this.books.push(book);
  }

  getBooks(): Book[] {
    return this.books;
  }
}

const library = new Library();
library.addBook({ id: 1, title: "1984", author: "George Orwell" });
library.addBook({ id: 2, title: "Brave New World", author: "Aldous Huxley" });

console.log(library.getBooks());

5. Key Takeaways

  • Use type aliases and interfaces to define object shapes.
  • Use classes for reusable blueprints with methods and properties.
  • Leverage function overloads for handling multiple input types.
  • Apply access modifiers (public, private, readonly) for better data encapsulation.

πŸ’‘ Next Steps:

  • Explore Generics to make reusable and type-safe components.
  • Learn about Decorators for advanced object manipulation.
  • Start building real-world applications like APIs or UI components.

πŸ”¨πŸ€–πŸ”§ Let me know if you need deeper insights into any topic!

Here’s a tutorial for debugging TypeScript code and working with tsconfig.json! πŸš€


Debugging and tsconfig.json in TypeScript

Debugging TypeScript applications and configuring the TypeScript compiler are essential skills for efficient development. This tutorial covers:

  1. Debugging techniques in TypeScript.
  2. Configuring the compiler using the tsconfig.json file.

1. Debugging TypeScript Code

1.1 Setting Up Debugging Environment

Step 1: Install Dependencies

Make sure you have Node.js and TypeScript installed:

npm install -g typescript
npm install -g ts-node

πŸ’‘ Tip: Use ts-node to execute TypeScript files directly without compiling.


Step 2: Create a Sample Program

Create a file example.ts:

function add(a: number, b: number): number {
  return a + b;
}

const result = add(10, 20);
console.log(result);

Step 3: Enable Source Maps for Debugging

  1. Add the following to your tsconfig.json (if it doesn’t exist, create one):
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ES6"
  }
}
  1. Compile your code:
tsc example.ts
  1. This generates a .js.map file alongside the compiled JavaScript file.

Step 4: Debug with Node.js

  1. Run the JavaScript file with the --inspect flag:
node --inspect example.js
  1. Open Chrome DevTools by visiting:
chrome://inspect
  1. Click on Open dedicated DevTools for Node to start debugging!

Step 5: Debug in Visual Studio Code

  1. Open VS Code and press F5.
  2. Select Node.js for the environment.
  3. Update the launch.json in .vscode/ directory:
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/example.ts",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "outFiles": ["${workspaceFolder}/**/*.js"],
      "sourceMaps": true
    }
  ]
}
  1. Press F5 to start debugging!

1.6 Debugging Tips

Adding Breakpoints

Click on the line number in VS Code to add a breakpoint.

Inspect Variables

Use the Variables tab in the debugger panel to inspect variable values.

Watch Expressions

Monitor specific variables or expressions in the Watch tab.

Console Logs

Use console.log() for quick debugging when source maps are not needed.


2. Configuring TypeScript with tsconfig.json

The tsconfig.json file defines compiler options for your TypeScript project.


2.1 Generating tsconfig.json

tsc --init

πŸ’‘ This creates a default configuration file in the project directory.


2.2 Key Compiler Options

Basic Settings

{
  "compilerOptions": {
    "target": "ES6",                 // Target JavaScript version
    "module": "CommonJS",            // Module system (e.g., ES modules or CommonJS)
    "outDir": "./dist",              // Output directory for compiled files
    "rootDir": "./src",              // Root directory for source files
    "strict": true,                  // Enables all strict type-checking options
    "esModuleInterop": true,         // Allows default imports for CommonJS modules
    "allowJs": true,                 // Allows JavaScript files in the project
    "checkJs": true,                 // Type-check JavaScript files
    "sourceMap": true,               // Generates source maps for debugging
    "noImplicitAny": true,           // Requires type annotations for variables
    "strictNullChecks": true,        // Ensures `null` and `undefined` are handled
    "moduleResolution": "node",      // Resolves modules using Node.js-style resolution
    "noUnusedLocals": true,          // Warns about unused local variables
    "noUnusedParameters": true,      // Warns about unused function parameters
    "noImplicitReturns": true,       // Ensures functions return a value
    "skipLibCheck": true,            // Skips type-checking of declaration files
    "resolveJsonModule": true        // Allows importing JSON files
  }
}

2.3 Useful Options Explained

1. Target JavaScript Version

"target": "ES6"

Transpiles TypeScript to ES6 JavaScript for modern browsers and environments.


2. Output Directory

"outDir": "./dist"

Outputs compiled JavaScript files to the dist directory.


3. Root Directory

"rootDir": "./src"

Specifies where the TypeScript source files are located.


4. Source Maps for Debugging

"sourceMap": true

Generates .map files, enabling debugging in source TypeScript instead of compiled JavaScript.


5. Strict Type Checking

"strict": true

Enables strict type-checking for safer and error-free code.


6. Module Resolution

"module": "CommonJS"

Specifies the module system, commonly used with Node.js.


2.4 Including and Excluding Files

Include Files

"include": ["src/**/*"]

Includes all files in the src directory.

Exclude Files

"exclude": ["node_modules", "dist"]

Excludes files and folders like node_modules and build output directories.


2.5 Custom Paths and Aliases

"baseUrl": "./src",
"paths": {
  "@utils/*": ["utils/*"],
  "@components/*": ["components/*"]
}

Allows importing modules like:

import { helper } from "@utils/helper";

2.6 Extending Configurations

"extends": "./base-tsconfig.json"

Inherits settings from a base configuration file.


3. Best Practices for Debugging and Configuration

  1. Use Source Maps
    Always enable source maps for better debugging experiences.

  2. Keep Configurations Organized
    Split configurations for dev and production using separate tsconfig files:

    • tsconfig.dev.json
    • tsconfig.prod.json
  3. Use Linting Tools
    Use ESLint with TypeScript for consistent code style and error prevention.

  4. Optimize for Production
    Disable source maps and enable minification for production builds.


Summary Table of Important Settings

Setting Purpose
target Specifies JavaScript version to compile to.
module Defines module system (e.g., CommonJS, ES6).
outDir Sets the output folder for compiled files.
strict Enables strict type checking.
sourceMap Generates source maps for debugging.
baseUrl and paths Configures module aliases for cleaner imports.
exclude Excludes files/folders like node_modules.

πŸ”¨πŸ€–πŸ”§ Let me know if you need more insights on debugging or configurations!

Here’s a tutorial for classes in TypeScript! πŸš€


Classes in TypeScript

Classes in TypeScript provide a blueprint for creating objects with properties and methods. They bring object-oriented programming (OOP) concepts like inheritance, encapsulation, and polymorphism into TypeScript.


1. Creating a Basic Class

class Person {
  name: string; // Property
  age: number;  // Property

  // Constructor
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // Method
  greet(): string {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

const person1 = new Person("Alice", 25);
console.log(person1.greet()); // Output: Hello, my name is Alice and I am 25 years old.

Explanation:

  1. Properties: Variables inside the class (name, age).
  2. Constructor: Initializes properties when creating a new object.
  3. Methods: Functions that operate on the class (greet()).

2. Access Modifiers

TypeScript provides modifiers to control the visibility of class members:

Modifier Accessibility
public Accessible everywhere (default behavior).
private Accessible only inside the class.
protected Accessible inside the class and derived (child) classes.

Example

class Person {
  public name: string;       // Accessible everywhere
  private age: number;       // Accessible only within this class
  protected job: string;     // Accessible within this and derived classes

  constructor(name: string, age: number, job: string) {
    this.name = name;
    this.age = age;
    this.job = job;
  }

  public displayInfo(): string {
    return `${this.name} is a ${this.job}.`;
  }
}

const person1 = new Person("Alice", 25, "Developer");
console.log(person1.displayInfo()); // βœ… Output: Alice is a Developer.
// console.log(person1.age);        ❌ Error: 'age' is private

3. Readonly Properties

Properties marked as readonly cannot be modified after initialization.

class Book {
  readonly title: string;

  constructor(title: string) {
    this.title = title;
  }

  displayTitle(): string {
    return this.title;
  }
}

const book = new Book("TypeScript Handbook");
console.log(book.displayTitle()); // Output: TypeScript Handbook

// book.title = "JavaScript Guide"; ❌ Error: Cannot assign to 'title' because it is readonly.

4. Getters and Setters

Getters and setters control access to private properties.

class Account {
  private balance: number;

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

  // Getter
  get Balance(): number {
    return this.balance;
  }

  // Setter
  set Balance(amount: number) {
    if (amount >= 0) {
      this.balance = amount;
    } else {
      throw new Error("Balance cannot be negative!");
    }
  }
}

const account = new Account(1000);
console.log(account.Balance); // Output: 1000

account.Balance = 500; // βœ… Allowed
console.log(account.Balance); // Output: 500

// account.Balance = -100; // ❌ Error: Balance cannot be negative!

5. Inheritance

Classes can inherit properties and methods from another class using the extends keyword.

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound(): void {
    console.log("Animal makes a sound");
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    super(name); // Calls parent constructor
    this.breed = breed;
  }

  bark(): void {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.makeSound(); // Output: Animal makes a sound
dog.bark();      // Output: Buddy barks.

6. Abstract Classes

Abstract classes provide a base structure but cannot be instantiated directly. They are used for inheritance.

abstract class Shape {
  abstract area(): number; // Abstract method (no implementation)

  display(): void {
    console.log("Displaying shape");
  }
}

class Rectangle extends Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.area()); // Output: 200

πŸ’‘ Key Takeaway:

  • Abstract methods must be implemented in derived classes.

7. Interfaces with Classes

Interfaces define the structure that a class should follow.

interface Animal {
  name: string;
  makeSound(): void;
}

class Dog implements Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound(): void {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy");
dog.makeSound(); // Output: Buddy barks.

8. Static Members

Static members belong to the class itself rather than its instances.

class MathUtils {
  static PI: number = 3.14;

  static calculateArea(radius: number): number {
    return this.PI * radius * radius;
  }
}

console.log(MathUtils.PI); // Output: 3.14
console.log(MathUtils.calculateArea(5)); // Output: 78.5

9. Generic Classes

Generics make classes reusable with different types.

class Box<T> {
  contents: T;

  constructor(contents: T) {
    this.contents = contents;
  }

  getContents(): T {
    return this.contents;
  }
}

const stringBox = new Box<string>("Hello");
console.log(stringBox.getContents()); // Output: Hello

const numberBox = new Box<number>(42);
console.log(numberBox.getContents()); // Output: 42

10. Private Constructor and Singleton Pattern

A singleton pattern ensures only one instance of a class exists.

class Singleton {
  private static instance: Singleton;

  private constructor() {} // Prevents creating an object outside the class

  static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // Output: true

11. Summary Table

Feature Syntax Example
Class Declaration class Person { constructor(name: string) {} }
Access Modifiers public, private, protected
Readonly Property readonly id: number;
Getters/Setters get name(): string {} / set name(value: string) {}
Inheritance class Dog extends Animal {}
Abstract Class abstract class Shape {}
Interfaces with Classes class Dog implements Animal {}
Static Members static PI = 3.14;
Generic Classes class Box<T> {}
Singleton Pattern private constructor() and static getInstance()

πŸ’‘ Next Steps:

  1. Explore decorators for metaprogramming.
  2. Learn about Dependency Injection for better design patterns.
  3. Start building real-world applications using this knowledge!

πŸ”¨πŸ€–πŸ”§ Need more examples? Let me know!

Here’s a tutorial for interfaces in TypeScript! πŸš€


Interfaces in TypeScript

An interface in TypeScript defines the structure or contract for an object, class, or function. It helps enforce type checking and ensures consistency in code.


1. Creating a Basic Interface

Example:

interface Person {
  name: string;
  age: number;
}

const person1: Person = {
  name: "Alice",
  age: 25,
};

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

πŸ’‘ Key Points:

  • The Person interface defines the required structure.
  • The object person1 must match the shape defined by the interface.

2. Optional Properties

Properties can be marked as optional using ?.

interface Car {
  brand: string;
  model: string;
  year?: number; // Optional property
}

const car1: Car = {
  brand: "Tesla",
  model: "Model S",
};

const car2: Car = {
  brand: "Ford",
  model: "Mustang",
  year: 2023,
};

console.log(car1); // βœ… Valid
console.log(car2); // βœ… Valid

3. Readonly Properties

Use readonly to prevent modification of properties after initialization.

interface User {
  readonly id: number;
  username: string;
}

const user: User = { id: 101, username: "Alice" };

// user.id = 102; ❌ Error: Cannot assign to 'id' because it is readonly.
user.username = "Bob"; // βœ… Allowed

4. Functions in Interfaces

Interfaces can define function signatures.

interface MathOperation {
  (a: number, b: number): number; // Function signature
}

const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;

console.log(add(10, 5));      // Output: 15
console.log(subtract(10, 5)); // Output: 5

5. Methods in Interfaces

Define methods within interfaces:

interface Animal {
  name: string;
  speak(): void;
}

const dog: Animal = {
  name: "Buddy",
  speak() {
    console.log(`${this.name} barks.`);
  },
};

dog.speak(); // Output: Buddy barks.

6. Index Signatures

Allow objects to have dynamic keys with specific value types.

interface Dictionary {
  [key: string]: string; // Dynamic key of type string with string value
}

const translations: Dictionary = {
  hello: "Hola",
  world: "Mundo",
};

console.log(translations.hello); // Output: Hola

7. Extending Interfaces

Interfaces can inherit properties from other interfaces using extends.

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

const dog: Dog = {
  name: "Buddy",
  breed: "Golden Retriever",
};

console.log(dog.name);  // Output: Buddy
console.log(dog.breed); // Output: Golden Retriever

πŸ’‘ Key Takeaway: Multiple interfaces can be combined for scalability.


8. Multiple Inheritance

Interfaces support multiple inheritance.

interface Printer {
  print(): void;
}

interface Scanner {
  scan(): void;
}

interface Copier extends Printer, Scanner {
  copy(): void;
}

const machine: Copier = {
  print() {
    console.log("Printing...");
  },
  scan() {
    console.log("Scanning...");
  },
  copy() {
    console.log("Copying...");
  },
};

machine.print(); // Output: Printing...
machine.scan();  // Output: Scanning...
machine.copy();  // Output: Copying...

9. Interfaces vs Type Aliases

Both interfaces and type aliases define object shapes, but there are differences.

Feature Interface Type Alias
Extensibility Can be extended using extends. Cannot be extended once defined.
Declaration Merging Supports merging automatically. Does not mergeβ€”re-declaration throws an error.
Complex Types Best for object shapes and method contracts. Suitable for unions, tuples, and intersections.
Example interface Person { name: string; } type Person = { name: string; };

10. Implementing Interfaces in Classes

Classes can implement interfaces to enforce structure.

interface Animal {
  name: string;
  makeSound(): void;
}

class Dog implements Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound(): void {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy");
dog.makeSound(); // Output: Buddy barks.

11. Extending Multiple Interfaces in Classes

interface Person {
  name: string;
  age: number;
}

interface Employee {
  jobTitle: string;
  salary: number;
}

class Manager implements Person, Employee {
  name: string;
  age: number;
  jobTitle: string;
  salary: number;

  constructor(name: string, age: number, jobTitle: string, salary: number) {
    this.name = name;
    this.age = age;
    this.jobTitle = jobTitle;
    this.salary = salary;
  }

  displayInfo(): string {
    return `${this.name}, ${this.jobTitle}, earns $${this.salary}`;
  }
}

const manager = new Manager("Alice", 30, "Project Manager", 90000);
console.log(manager.displayInfo()); // Output: Alice, Project Manager, earns $90000

12. Using Generics with Interfaces

Interfaces can work with generics to support multiple types.

interface Box<T> {
  content: T;
}

const stringBox: Box<string> = { content: "Hello" };
const numberBox: Box<number> = { content: 42 };

console.log(stringBox.content); // Output: Hello
console.log(numberBox.content); // Output: 42

13. Function Interfaces with Generics

interface Operation<T> {
  (a: T, b: T): T;
}

const add: Operation<number> = (x, y) => x + y;
const concat: Operation<string> = (x, y) => x + y;

console.log(add(10, 20));        // Output: 30
console.log(concat("Hi", " TS")); // Output: Hi TS

14. Summary Table of Interface Features

Feature Example
Basic Interface interface Person { name: string; }
Optional Properties age?: number
Readonly Properties readonly id: number
Function Signature (x: number, y: number): number
Index Signatures [key: string]: string
Extending Interfaces interface Dog extends Animal {}
Multiple Inheritance interface Copier extends Printer, Scanner {}
Generic Interfaces interface Box<T> { content: T }
Class Implementation class Dog implements Animal { makeSound(): void {} }

Next Steps

  • Explore advanced types like intersections, mapped types, and conditional types.
  • Build reusable UI components or APIs with TypeScript interfaces.
  • Dive into decorators and dependency injection for advanced patterns.

πŸ”¨πŸ€–πŸ”§ Need more examples? Let me know!

Here’s a tutorial for namespaces in TypeScript! πŸš€


Namespaces in TypeScript

Namespaces in TypeScript are used to group related code (variables, functions, interfaces, and classes) under a single namespace to avoid naming conflicts. They are particularly useful in large codebases where multiple modules or libraries might define variables or types with the same name.


1. Creating a Namespace

Example:

namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }

  export function subtract(a: number, b: number): number {
    return a - b;
  }
}

console.log(MathUtils.add(10, 5));      // Output: 15
console.log(MathUtils.subtract(10, 5)); // Output: 5

πŸ’‘ Key Points:

  • namespace defines a logical grouping.
  • export keyword makes functions, classes, or variables accessible outside the namespace.
  • Access exported members using NamespaceName.memberName.

2. Nested Namespaces

Namespaces can be nested inside other namespaces.

Example:

namespace App {
  export namespace Models {
    export interface User {
      id: number;
      name: string;
    }
  }

  export namespace Services {
    export function getUser(id: number): Models.User {
      return { id, name: "Alice" };
    }
  }
}

// Usage
const user = App.Services.getUser(1);
console.log(user); // Output: { id: 1, name: "Alice" }

πŸ’‘ Key Points:

  • Use nested namespaces to organize related code into subgroups.
  • Access nested namespaces with dot notation.

3. Merging Namespaces

Namespaces with the same name automatically merge their declarations.

Example:

namespace Library {
  export class Book {
    title: string;

    constructor(title: string) {
      this.title = title;
    }
  }
}

namespace Library {
  export function getBooks(): string[] {
    return ["1984", "Brave New World"];
  }
}

// Access merged namespace
const book = new Library.Book("The Catcher in the Rye");
console.log(book.title); // Output: The Catcher in the Rye

const books = Library.getBooks();
console.log(books); // Output: [ '1984', 'Brave New World' ]

πŸ’‘ Key Points:

  • Multiple namespace declarations with the same name are merged into one.
  • Useful for extending functionality without modifying the original namespace.

4. Namespace Aliases

Use aliases for easier access to deeply nested namespaces.

Example:

namespace Animals {
  export namespace Mammals {
    export class Dog {
      bark() {
        console.log("Woof!");
      }
    }
  }
}

import Mammal = Animals.Mammals;

const dog = new Mammal.Dog();
dog.bark(); // Output: Woof!

πŸ’‘ Key Points:

  • Aliases make code cleaner when dealing with deeply nested namespaces.
  • Use the import keyword to create aliases.

5. Splitting Namespaces Across Files

Namespaces can span across multiple files using the reference directive.

File 1: shapes.ts

namespace Shapes {
  export interface Circle {
    radius: number;
  }

  export function calculateArea(circle: Circle): number {
    return Math.PI * circle.radius * circle.radius;
  }
}

File 2: app.ts

/// <reference path="shapes.ts" />

const circle: Shapes.Circle = { radius: 10 };
console.log(Shapes.calculateArea(circle)); // Output: 314.1592653589793

πŸ’‘ Key Points:

  • Use /// <reference path="file.ts" /> to link files.
  • The TypeScript compiler resolves dependencies when files are compiled together.

Compile the Files Together

tsc --outFile output.js shapes.ts app.ts

6. Combining Namespaces and Modules

Namespaces work with ES Modules for scalability.

Example:

File: utils.ts

export namespace MathUtils {
  export function multiply(a: number, b: number): number {
    return a * b;
  }
}

File: main.ts

import { MathUtils } from "./utils";

console.log(MathUtils.multiply(10, 2)); // Output: 20

πŸ’‘ Key Points:

  • When using export and import, namespaces behave like modules.
  • Namespaces are ideal for projects without module bundlers like Webpack or Vite.

7. Differences Between Namespaces and Modules

Feature Namespaces Modules
Scope Encapsulates code within a logical grouping. Encapsulates code using file-level scope.
Dependencies Uses /// <reference path="file.ts" /> for dependencies. Uses ES6 import/export for dependency management.
Use Case Suitable for global libraries or scripts. Suitable for modern modular applications.
Compilation Requires a single output file for all namespaces. Supports multiple output files with module loaders.
Alias Support Supports namespace aliases using import. Uses ES6 imports for shorter paths.

πŸ’‘ Tip: Use modules for modern applications and namespaces for legacy projects or libraries.


8. When to Use Namespaces?

Namespaces are best suited for:

  1. Global Libraries - Useful when code needs to be accessible globally (e.g., third-party SDKs).
  2. Legacy Applications - Where module bundling tools (like Webpack) are not used.
  3. Internal Projects - Useful for organizing large codebases without external dependencies.

9. Example: Namespace for Utilities

namespace Utils {
  export namespace MathOperations {
    export function add(a: number, b: number): number {
      return a + b;
    }
  }

  export namespace StringOperations {
    export function toUpperCase(str: string): string {
      return str.toUpperCase();
    }
  }
}

// Usage
console.log(Utils.MathOperations.add(10, 20));         // Output: 30
console.log(Utils.StringOperations.toUpperCase("hi")); // Output: HI

10. Summary Table

Feature Example
Basic Namespace namespace MathUtils { export function add() {} }
Nested Namespace namespace App.Models {}
Merging Namespaces namespace Library {} (merged declarations)
Aliases import Utils = Namespace.Utils;
File Splitting /// <reference path="file.ts" />
Combining with Modules export namespace MathUtils {} with ES6 imports.

Next Steps

  1. Explore modules and understand when to prefer them over namespaces.
  2. Learn about type declarations for third-party libraries using namespaces.
  3. Build scalable TypeScript libraries with namespaces and modules combined.

πŸ”¨πŸ€–πŸ”§ Need more examples or explanations? Just ask!

Here’s a tutorial for Generics in TypeScript! πŸš€


Generics in TypeScript

Generics in TypeScript allow you to write reusable and type-safe code by creating components (functions, classes, or interfaces) that work with multiple data types. They provide type flexibility while maintaining type checking.


1. Why Use Generics?

Consider the following function that returns the first element of an array:

function getFirstElement(arr: any[]): any {
  return arr[0];
}

console.log(getFirstElement([1, 2, 3])); // Output: 1
console.log(getFirstElement(["a", "b", "c"])); // Output: "a"

Problems:

  1. No Type Safety: The return type is any, so TypeScript cannot enforce type checking.
  2. Loss of Intellisense: Autocomplete and type hints are lost.

Solution with Generics

function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

console.log(getFirstElement<number>([1, 2, 3]));    // Output: 1
console.log(getFirstElement<string>(["a", "b"])); // Output: "a"

πŸ’‘ Key Points:

  • <T> defines a generic type variable.
  • T is used as a placeholder for the actual type.
  • Type inference automatically detects types, improving type safety.

2. Generic Functions

2.1 Basic Generic Function

function identity<T>(value: T): T {
  return value;
}

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

2.2 Type Inference

TypeScript can automatically infer types based on the arguments passed:

console.log(identity(100));      // Output: 100
console.log(identity("World")); // Output: World

3. Generic Interfaces

3.1 Defining a Generic Interface

interface Box<T> {
  content: T;
}

const stringBox: Box<string> = { content: "Hello" };
const numberBox: Box<number> = { content: 42 };

console.log(stringBox.content); // Output: Hello
console.log(numberBox.content); // Output: 42

4. Generic Classes

4.1 Creating a Generic Class

class DataStore<T> {
  private data: T[] = [];

  addItem(item: T): void {
    this.data.push(item);
  }

  getItems(): T[] {
    return this.data;
  }
}

const numberStore = new DataStore<number>();
numberStore.addItem(1);
numberStore.addItem(2);
console.log(numberStore.getItems()); // Output: [1, 2]

const stringStore = new DataStore<string>();
stringStore.addItem("A");
stringStore.addItem("B");
console.log(stringStore.getItems()); // Output: ['A', 'B']

5. Generic Constraints

You can restrict types that can be passed to generics using extends.

Example 1: Constraint with Interfaces

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(item: T): void {
  console.log(item.length);
}

logLength([1, 2, 3]); // Output: 3 (array has length)
// logLength(10);     ❌ Error: Number doesn't have 'length'.
logLength({ length: 5 }); // Output: 5

Example 2: Constraint with Properties

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "Alice", age: 25 };
console.log(getProperty(person, "name")); // Output: Alice
// console.log(getProperty(person, "height")); ❌ Error: 'height' is not in 'person'.

6. Using Default Types in Generics

Example:

interface Response<T = string> {
  data: T;
  success: boolean;
}

const response1: Response = { data: "OK", success: true }; // Defaults to string
const response2: Response<number> = { data: 200, success: true }; // Custom type

7. Generic Utility Types

TypeScript provides built-in utility types that use generics.

1. Partial

Makes all properties optional.

interface Person {
  name: string;
  age: number;
}

const partialPerson: Partial<Person> = {
  name: "Alice", // age is optional
};

2. Readonly

Makes all properties readonly.

const readonlyPerson: Readonly<Person> = {
  name: "Bob",
  age: 30,
};

// readonlyPerson.age = 40; ❌ Error: Cannot modify readonly property.

3. Record<K, T>

Creates a type with keys of type K and values of type T.

const scores: Record<string, number> = {
  Alice: 95,
  Bob: 85,
};

4. Pick<T, K>

Selects specific properties from a type.

type PersonName = Pick<Person, "name">; // Includes only 'name'

5. Omit<T, K>

Excludes specific properties from a type.

type PersonWithoutAge = Omit<Person, "age">; // Removes 'age'

8. Generic Functions with Multiple Types

Example:

function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const result = merge({ name: "Alice" }, { age: 25 });
console.log(result); // Output: { name: "Alice", age: 25 }

9. Generics in Functions with Promises

Example:

function fetchData<T>(url: string): Promise<T> {
  return fetch(url).then((res) => res.json());
}

fetchData<{ name: string; age: number }>("https://api.example.com/user")
  .then((data) => console.log(data.name)) // Type-safe access to properties
  .catch((err) => console.error(err));

10. Summary Table

Feature Example
Generic Function function identity<T>(value: T): T {}
Generic Interface interface Box<T> { content: T; }
Generic Class class DataStore<T> { addItem(item: T): void {} }
Constraints T extends Lengthwise
Default Generic Type interface Response<T = string>
Utility Types Partial<T>, Readonly<T>, Record<K, T>, Pick<T, K>, Omit<T, K>

Next Steps

  1. Practice utility types for transforming data structures.
  2. Learn Mapped Types and Conditional Types for more flexibility.
  3. Use Generics in real-world projects like API handlers or reusable UI components.

πŸ”¨πŸ€–πŸ”§ Need more examples? Let me know!

Here’s a tutorial for decorators in TypeScript! πŸš€


Decorators in TypeScript

Decorators in TypeScript are special functions that can be attached to classes, methods, properties, accessors, and parameters to add metadata or functionality at runtime.

Decorators are heavily used in Angular, NestJS, and other frameworks to simplify code and enhance reusability.


1. Enabling Decorators

Step 1: Enable Decorators in tsconfig.json

Add the following in the tsconfig.json file:

{
  "compilerOptions": {
    "experimentalDecorators": true,  // Enable decorators
    "emitDecoratorMetadata": true   // Enable metadata reflection
  }
}

Step 2: Install Reflect Metadata (Optional)

npm install reflect-metadata

Step 3: Import Reflect Metadata (if required):

import "reflect-metadata";

2. Types of Decorators

Decorators can be applied to:

  1. Class Decorators
  2. Method Decorators
  3. Property Decorators
  4. Accessor Decorators
  5. Parameter Decorators

3. Class Decorators

A class decorator is applied to an entire class to modify or enhance its behavior.

Example: Adding Metadata

function Logger(constructor: Function) {
  console.log("Logging...");
  console.log(constructor);
}

@Logger
class Person {
  constructor(public name: string) {
    console.log(`Person created: ${this.name}`);
  }
}

const person = new Person("Alice");
// Output:
// Logging...
// class Person { ... }
// Person created: Alice

πŸ’‘ Key Points:

  • @Logger is the decorator applied to the Person class.
  • It logs the class constructor and metadata.

Example: Modifying a Class

function Component(template: string, selector: string) {
  return function (constructor: any) {
    const element = document.querySelector(selector);
    if (element) {
      element.innerHTML = template;
    }
  };
}

@Component("<h1>Hello World!</h1>", "#app")
class AppComponent {}

πŸ’‘ Key Points:

  • Decorators can modify DOM elements dynamically.
  • Used in Angular for component rendering.

4. Method Decorators

A method decorator can modify or extend the behavior of a method.

Example: Logging Method Calls

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method: ${propertyKey}`);
    console.log(`Arguments: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);
// Output:
// Method: add
// Arguments: [2,3]
// Result: 5

πŸ’‘ Key Points:

  • Decorator logs method calls, parameters, and results.
  • Useful for debugging and analytics.

5. Property Decorators

A property decorator is used to add metadata or validation to a property.

Example: Adding Metadata

function ReadOnly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
  });
}

class User {
  @ReadOnly
  username: string = "admin";
}

const user = new User();
// user.username = "newAdmin"; ❌ Error: Cannot assign to 'username' because it is read-only.
console.log(user.username); // Output: admin

πŸ’‘ Key Points:

  • Prevents modifications to read-only properties.
  • Commonly used for validation rules.

6. Accessor Decorators

An accessor decorator modifies the behavior of getters and setters.

Example: Validating Setter Values

function Positive(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalSet = descriptor.set;

  descriptor.set = function (value: number) {
    if (value < 0) {
      throw new Error("Value must be positive!");
    }
    originalSet!.call(this, value);
  };
}

class Account {
  private _balance: number = 0;

  @Positive
  set balance(value: number) {
    this._balance = value;
  }

  get balance(): number {
    return this._balance;
  }
}

const acc = new Account();
acc.balance = 100; // βœ… Allowed
console.log(acc.balance); // Output: 100
// acc.balance = -50; ❌ Error: Value must be positive!

πŸ’‘ Key Points:

  • Used for input validation and data constraints.

7. Parameter Decorators

A parameter decorator is used to track or validate parameters of a method.

Example: Parameter Metadata

function LogParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter index: ${parameterIndex} in method: ${propertyKey}`);
}

class Person {
  greet(@LogParameter message: string) {
    console.log(message);
  }
}

const p = new Person();
p.greet("Hello");
// Output: Parameter index: 0 in method: greet

πŸ’‘ Key Points:

  • Tracks and logs metadata for method parameters.

8. Combining Decorators

Multiple decorators can be stacked on a single target.

function LogClass(constructor: Function) {
  console.log(`Class: ${constructor.name}`);
}

function LogMethod(target: any, propertyKey: string) {
  console.log(`Method: ${propertyKey}`);
}

@LogClass
class Employee {
  @LogMethod
  work() {
    console.log("Working...");
  }
}

const emp = new Employee();
emp.work();

// Output:
// Class: Employee
// Method: work
// Working...

πŸ’‘ Key Points:

  • Decorators execute top-to-bottom during declaration and bottom-to-top during execution.

9. Decorators with Metadata Reflection

With Reflect Metadata, decorators can read and write metadata dynamically.

Example:

import "reflect-metadata";

function Required(target: any, key: string) {
  Reflect.defineMetadata("required", true, target, key);
}

function Validate(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const required = Reflect.getMetadata("required", target, key);
    if (required && args[0] === undefined) {
      throw new Error("Missing required parameter!");
    }
    return original.apply(this, args);
  };
}

class User {
  @Required
  name!: string;

  @Validate
  setName(name: string) {
    this.name = name;
  }
}

const user = new User();
user.setName("Alice"); // βœ… Works
// user.setName();     ❌ Error: Missing required parameter!

10. Summary Table

Decorator Type Purpose
Class Decorator Enhances a class definition (e.g., metadata, logging, etc.)
Method Decorator Modifies behavior of methods (e.g., logging calls, caching).
Property Decorator Adds metadata or validation for properties (e.g., read-only).
Accessor Decorator Validates or transforms getters/setters.
Parameter Decorator Tracks metadata for method parameters (e.g., logging parameter positions).

Next Steps

  1. Explore Angular decorators like @Component and @Injectable.
  2. Learn metadata reflection with reflect-metadata.
  3. Build real-world applications with NestJS or custom decorators for validations and caching.

πŸ”¨πŸ€–πŸ”§ Need more examples? Let me know!

Here’s a list of the most used decorators in Next.js (with TypeScript) and related frameworks like NestJS for backend development. πŸš€


1. Next.js Decorators

Although Next.js does not natively support decorators like NestJS, decorators are commonly used in combination with class-based controllers or custom handlers when integrating libraries such as type-graphql or tRPC.

Common Use Cases for Decorators in Next.js

  1. API Request Validation
  2. Role-Based Access Control (RBAC)
  3. Logging Middleware
  4. Dependency Injection

1.1 Custom Middleware Decorator

export function Middleware(handler: Function) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (req: any, res: any) {
      await handler(req, res); // Middleware logic
      return originalMethod.apply(this, [req, res]);
    };
  };
}

// Example Usage:
class APIController {
  @Middleware(async (req: any, res: any) => {
    if (!req.headers.authorization) {
      res.status(401).json({ error: "Unauthorized" });
      throw new Error("Unauthorized");
    }
  })
  async getUser(req: any, res: any) {
    res.status(200).json({ user: "Alice" });
  }
}

1.2 Role-Based Access Control (RBAC)

export function Role(role: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (req: any, res: any) {
      const userRole = req.headers.role; // Assume role is sent in headers
      if (userRole !== role) {
        res.status(403).json({ error: "Forbidden" });
        throw new Error("Access Denied");
      }
      return originalMethod.apply(this, [req, res]);
    };
  };
}

// Example Usage:
class APIController {
  @Role("admin")
  async deleteUser(req: any, res: any) {
    res.status(200).json({ message: "User deleted." });
  }
}

2. Common Decorators in NestJS (often used with Next.js API Routes)

2.1 Controller Decorator (@Controller)

Defines a controller to handle incoming requests.

@Controller('users')
export class UserController {
  @Get()
  findAll() {
    return "List of users";
  }
}

2.2 HTTP Method Decorators (@Get, @Post, etc.)

Used to map routes to methods inside a controller.

@Controller('users')
export class UserController {
  @Get(':id') // HTTP GET request
  findOne(@Param('id') id: string) {
    return `User with ID: ${id}`;
  }

  @Post() // HTTP POST request
  create(@Body() data: any) {
    return { message: "User created", data };
  }
}

2.3 Parameter Decorators

Decorator Description
@Param() Retrieves route parameters (e.g., /user/:id).
@Body() Extracts body data from incoming requests.
@Query() Accesses query string parameters (e.g., ?key=value).
@Req() Provides access to the request object.
@Res() Provides access to the response object.

Example:

@Controller('users')
export class UserController {
  @Post()
  createUser(@Body() body: any) {
    return { message: "User Created", data: body };
  }

  @Get(':id')
  getUser(@Param('id') id: string) {
    return { message: `Fetching user with ID: ${id}` };
  }
}

2.4 Middleware Decorator (@UseGuards)

Used to apply guards for authentication and authorization.

@UseGuards(AuthGuard) // Applies authentication middleware
@Post('profile')
getProfile(@Req() req: any) {
  return req.user;
}

2.5 Validation Decorators (from class-validator)

Common decorators for validating incoming data:

Decorator Purpose
@IsString() Ensures the value is a string.
@IsEmail() Validates that the value is a valid email address.
@MinLength() Enforces a minimum length for the value.
@MaxLength() Enforces a maximum length for the value.
@IsOptional() Marks the property as optional.
@IsNumber() Validates that the value is a number.

Example:

import { IsString, IsEmail, MinLength } from "class-validator";

class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @MinLength(6)
  password: string;
}

2.6 Dependency Injection Decorators

  • @Injectable() - Makes a class injectable as a service.
  • @Inject() - Injects dependencies into a class.
@Injectable()
export class UserService {
  getUsers() {
    return [{ id: 1, name: "Alice" }];
  }
}

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll() {
    return this.userService.getUsers();
  }
}

3. Summary Table of Most Used Decorators

Decorator Purpose
@Controller() Declares a controller in NestJS.
@Get(), @Post() Maps HTTP routes to methods.
@Param(), @Body() Handles incoming request parameters and body data.
@UseGuards() Applies authentication or authorization guards to routes.
@Injectable() Declares a service or provider as injectable.
@Log Custom decorator for logging method calls (custom use case).
@Validate Validates input parameters dynamically (e.g., for validation rules).
@Required Ensures parameters are required using Reflect Metadata.

4. Final Thoughts

  • Next.js primarily uses middleware functions, but decorators are great for custom middleware, validation, and logging in API routes.
  • NestJS (often used alongside Next.js) provides a robust decorator-based system for building scalable applications.
  • Use class-validator and reflect-metadata to create type-safe and validated APIs.

πŸ”¨πŸ€–πŸ”§ Need help building a complete API with decorators? Just let me know!

Next.js no estΓ‘ eliminando directamente el soporte para decorators, sino que nunca ha tenido soporte nativo para ellos. Sin embargo, algunas decisiones en el diseΓ±o y evoluciΓ³n del framework explican por quΓ© decorators no se utilizan ampliamente en Next.js. AquΓ­ tienes las razones:


1. Enfoque Basado en Funciones y Hooks

Next.js se basa en el paradigma de programaciΓ³n funcional a travΓ©s de React y sus hooks.

  • Decorators son mΓ‘s comunes en arquitecturas orientadas a clases como NestJS o Angular.
  • React (y por lo tanto Next.js) promueve un enfoque basado en funciones y hooks como useState, useEffect y useContext para manejar el estado, efectos secundarios y lΓ³gica compartida.
  • Los hooks reemplazan la necesidad de decoradores en la mayorΓ­a de los casos al permitir composiciΓ³n de lΓ³gica reutilizable.

Ejemplo con hooks en lugar de decoradores:

function useAuth() {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    // Simulate authentication logic
    setUser({ id: 1, name: "Alice" });
  }, []);

  return user;
}

export default function Dashboard() {
  const user = useAuth();

  if (!user) return <p>Loading...</p>;
  return <h1>Welcome {user.name}</h1>;
}

Este ejemplo utiliza hooks en lugar de decoradores para manejar la autenticaciΓ³n.


2. EstΓ‘ndares ECMAScript y Decorators Experimentales

  • Los decorators en TypeScript estΓ‘n marcados como experimentales y todavΓ­a no estΓ‘n en el estΓ‘ndar final de JavaScript/ECMAScript.
  • Aunque se han propuesto en TC39 (Stage 3), su especificaciΓ³n sigue cambiando.
  • Esto crea problemas de compatibilidad con futuras versiones del lenguaje.
  • Next.js sigue los estΓ‘ndares mΓ‘s modernos y estables en lugar de depender de caracterΓ­sticas experimentales.

3. Preferencia por Middlewares en API Routes

En lugar de usar decoradores, Next.js recomienda el uso de middlewares en las API Routes para validaciΓ³n, autenticaciΓ³n, y autorizaciΓ³n.

Ejemplo: Middleware para autenticaciΓ³n

export default function handler(req, res) {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  res.status(200).json({ message: "Welcome!" });
}

Este middleware puede ser reutilizado fΓ‘cilmente en mΓΊltiples rutas.


4. Alternativas MΓ‘s Modernas: Middlewares y Higher-Order Functions (HOCs)

En lugar de decoradores, Next.js recomienda:

  1. Middlewares (como el ejemplo anterior).
  2. Higher-Order Functions (HOCs) para agregar funcionalidades reutilizables:

Ejemplo con HOC para permisos:

const withAuth = (handler) => (req, res) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  return handler(req, res);
};

export default withAuth((req, res) => {
  res.status(200).json({ message: "Protected route accessed" });
});

Este enfoque ofrece flexibilidad sin depender de decoradores experimentales.


5. IntegraciΓ³n con APIs Modernas como tRPC y GraphQL

En lugar de usar decoradores, Next.js promueve el uso de bibliotecas modernas como tRPC o GraphQL, que ofrecen tipado seguro sin necesidad de decoradores.

Ejemplo con tRPC:

import { createRouter } from "@trpc/server";

export const userRouter = createRouter()
  .query("getUser", {
    resolve() {
      return { id: 1, name: "Alice" };
    },
  })
  .mutation("addUser", {
    resolve({ input }) {
      return { id: 2, name: input.name };
    },
  });

6. Frameworks Orientados a Decorators: NestJS y TypeORM

Si necesitas trabajar con decorators, frameworks como NestJS o TypeORM pueden ser mejores opciones. Por ejemplo:

NestJS Decorators para API REST:

@Controller('users')
export class UserController {
  @Get()
  getAllUsers() {
    return "List of users";
  }

  @Post()
  createUser(@Body() body: any) {
    return { message: "User created", data: body };
  }
}

7. Simplicidad y OptimizaciΓ³n para SSR/SSG

Next.js prioriza caracterΓ­sticas como:

  • Server-Side Rendering (SSR).
  • Static Site Generation (SSG).
  • Incremental Static Regeneration (ISR).

Estas caracterΓ­sticas requieren un enfoque mΓ‘s declarativo y basado en funciones en lugar de clases con decoradores.


ConclusiΓ³n: ΒΏDeberΓ­as usar decoradores en Next.js?

Aunque puedes habilitar y usar decoradores en Next.js configurando experimentalDecorators en tsconfig.json, no es recomendable porque:

  1. No es compatible nativamente con React y Next.js.
  2. Alternativas como hooks y HOCs son mΓ‘s naturales en aplicaciones modernas basadas en React.
  3. Decorators siguen siendo experimentales y podrΓ­an cambiar en el futuro.
  4. LibrerΓ­as modernas como tRPC, class-validator y Zod manejan validaciones y tipado de manera mΓ‘s efectiva.

RecomendaciΓ³n

Si deseas aprovechar decorators en aplicaciones grandes con lΓ³gica backend avanzada, usa NestJS junto con Next.js. Para aplicaciones centradas en frontend y SSR, sigue usando hooks, HOCs, y middlewares en Next.js.

πŸ”¨πŸ€–πŸ”§ ΒΏNecesitas ejemplos especΓ­ficos para alguna implementaciΓ³n? Β‘AvΓ­same!

Here’s how to replace decorators in Next.js for:

  1. API Request Validation
  2. Role-Based Access Control (RBAC)
  3. Logging Middleware
  4. Dependency Injection (DI)

Using functional programming patterns (Middlewares, Higher-Order Functions, and Utility Functions), you can achieve similar results without relying on experimental decorators. πŸš€


1. API Request Validation

Instead of decorators, use middleware functions or validation libraries like Zod or Yup.

Example Using Zod for Validation

npm install zod
import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";

// Validation schema
const userSchema = z.object({
  name: z.string().min(3, "Name must be at least 3 characters long"),
  email: z.string().email("Invalid email format"),
});

// Validation middleware
const validateRequest =
  (schema: any) =>
  (handler: any) =>
  async (req: NextApiRequest, res: NextApiResponse) => {
    try {
      schema.parse(req.body); // Validate request body
      return handler(req, res);
    } catch (err) {
      return res.status(400).json({ error: err.errors });
    }
  };

// Usage
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ message: "User created successfully!" });
};

export default validateRequest(userSchema)(handler);

Why it works?

  • Achieves type-safe validation without decorators.
  • Uses Zod for schema-based validation, providing autocomplete and error handling.
  • Flexible and reusable middleware for validation.

2. Role-Based Access Control (RBAC)

Instead of decorators, use Higher-Order Functions (HOCs) for RBAC middleware.

RBAC Middleware Example

const withRole =
  (allowedRoles: string[]) =>
  (handler: any) =>
  async (req: NextApiRequest, res: NextApiResponse) => {
    const userRole = req.headers.role; // Simulate user role from headers

    if (!allowedRoles.includes(userRole as string)) {
      return res.status(403).json({ error: "Forbidden" });
    }

    return handler(req, res);
  };

// Usage Example
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ message: "Access granted!" });
};

export default withRole(["admin"])(handler);

Why it works?

  • Implements RBAC without decorators.
  • Reusable for multiple endpoints.
  • Allows dynamic roles based on route requirements.

3. Logging Middleware

Instead of decorators, use logging middleware with a Higher-Order Function.

Logging Middleware Example

const withLogging =
  (handler: any) =>
  async (req: NextApiRequest, res: NextApiResponse) => {
    console.log(`Request: ${req.method} ${req.url}`);
    console.log("Headers:", req.headers);
    console.log("Body:", req.body);

    const start = Date.now();
    const result = await handler(req, res);
    const duration = Date.now() - start;

    console.log(`Response time: ${duration}ms`);
    return result;
  };

// Usage Example
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ message: "Logging Example" });
};

export default withLogging(handler);

Why it works?

  • Logs requests and responses dynamically.
  • Tracks performance with response time monitoring.
  • Works like a decorator but uses a middleware function.

4. Dependency Injection (DI)

Instead of decorators like @Injectable(), use manual dependency injection via contexts or factories.

Example: Dependency Injection with Context API (Frontend)

import { createContext, useContext } from "react";

// Service
class UserService {
  getUser() {
    return { id: 1, name: "Alice" };
  }
}

// Context for DI
const UserServiceContext = createContext(new UserService());

export const useUserService = () => useContext(UserServiceContext);

// Component Usage
export default function Profile() {
  const userService = useUserService();
  const user = userService.getUser();

  return <h1>Hello, {user.name}</h1>;
}

Example: Dependency Injection for Backend (API)

class UserService {
  getUser() {
    return { id: 1, name: "Alice" };
  }
}

const createHandler = (userService: UserService) => {
  return async (req: NextApiRequest, res: NextApiResponse) => {
    const user = userService.getUser();
    res.status(200).json(user);
  };
};

const userService = new UserService();
export default createHandler(userService);

Why it works?

  • Provides dependency injection without decorators.
  • Works with contexts for frontend and factories for backend services.
  • Ensures testability and separation of concerns.

5. Combining Multiple Middlewares

You can combine multiple middlewares using utility functions for validation, RBAC, and logging.

const pipe =
  (...middlewares: any[]) =>
  (handler: any) =>
  middlewares.reduceRight((acc, mw) => mw(acc), handler);

// Combine Middlewares
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ message: "Success!" });
};

export default pipe(
  withLogging,
  withRole(["admin"]),
  validateRequest(userSchema)
)(handler);

Why it works?

  • Achieves decorator-like chaining using functions.
  • Combines validation, RBAC, and logging seamlessly.
  • Flexible, reusable, and supports modularity.

6. Summary Table: Decorators vs Middleware

Use Case Decorator Example Middleware Replacement
Validation @Validate() Middleware with Zod schema validation.
RBAC (Role-Based Access) @Role('admin') Middleware with role-based access using headers.
Logging @Log() Middleware for request and response logging.
Dependency Injection (DI) @Injectable() and @Inject() Manual DI using contexts (React) or factories (API).

7. Final Thoughts

While decorators are useful in frameworks like NestJS, Next.js focuses on functional programming patterns, favoring middlewares, HOCs, and custom hooks.

You can achieve similar behavior without decorators using these modern approaches:

  • Zod or Yup for validation.
  • HOCs and Middlewares for RBAC and logging.
  • Dependency Injection via contexts or factories.

When to Use Decorators?

If your project requires class-based design and decorators, consider combining Next.js with NestJS for backend APIs and Next.js for the frontend.

πŸ”¨πŸ€–πŸ”§ Need more examples or explanations? Let me know!

Here's a full example using advanced TypeScript features, including generics, decorators, interfaces, abstract classes, and metadata reflection. πŸš€


Scenario: Building a Library Management System

This example demonstrates a library management system that manages books, users, and transactions. It includes:

  1. Classes with Generics for reusable data storage.
  2. Decorators for logging, validation, and metadata reflection.
  3. Interfaces and Abstract Classes for extensibility and enforcing contracts.
  4. Modules and Namespaces for organization.

Step 1: Setup Environment

  1. Enable decorators in tsconfig.json:
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true
  }
}
  1. Install Metadata Reflection:
npm install reflect-metadata

1. Define Interfaces and Abstract Classes

interfaces.ts

export interface Identifiable {
  id: string;
}

export interface Book extends Identifiable {
  title: string;
  author: string;
  available: boolean;
}

export interface User extends Identifiable {
  name: string;
  email: string;
}

export interface Transaction extends Identifiable {
  bookId: string;
  userId: string;
  date: Date;
}

2. Create Generic Data Store

data-store.ts

export class DataStore<T extends Identifiable> {
  private items: Map<string, T> = new Map();

  add(item: T): void {
    this.items.set(item.id, item);
  }

  get(id: string): T | undefined {
    return this.items.get(id);
  }

  getAll(): T[] {
    return Array.from(this.items.values());
  }

  delete(id: string): void {
    this.items.delete(id);
  }
}

3. Decorators

decorators.ts

import "reflect-metadata";

// Log Execution
export function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Executing: ${key} with args: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result: ${JSON.stringify(result)}`);
    return result;
  };
}

// Validate Input
export function Validate(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    args.forEach((arg, index) => {
      const required = Reflect.getMetadata("required", target, key, index);
      if (required && (arg === null || arg === undefined)) {
        throw new Error(`Argument at index ${index} is required for method ${key}`);
      }
    });
    return originalMethod.apply(this, args);
  };
}

// Required Parameter
export function Required(target: any, key: string, parameterIndex: number) {
  Reflect.defineMetadata("required", true, target, key, parameterIndex);
}

4. Implement Services

book-service.ts

import { Book } from "./interfaces";
import { DataStore } from "./data-store";
import { Log, Required, Validate } from "./decorators";

export class BookService {
  private bookStore = new DataStore<Book>();

  @Log
  @Validate
  addBook(@Required book: Book): void {
    this.bookStore.add(book);
  }

  @Log
  getBooks(): Book[] {
    return this.bookStore.getAll();
  }

  @Log
  getAvailableBooks(): Book[] {
    return this.bookStore.getAll().filter((book) => book.available);
  }
}

user-service.ts

import { User } from "./interfaces";
import { DataStore } from "./data-store";
import { Log } from "./decorators";

export class UserService {
  private userStore = new DataStore<User>();

  @Log
  addUser(user: User): void {
    this.userStore.add(user);
  }

  @Log
  getUsers(): User[] {
    return this.userStore.getAll();
  }
}

transaction-service.ts

import { Transaction } from "./interfaces";
import { DataStore } from "./data-store";
import { Log } from "./decorators";

export class TransactionService {
  private transactionStore = new DataStore<Transaction>();

  @Log
  addTransaction(transaction: Transaction): void {
    this.transactionStore.add(transaction);
  }

  @Log
  getTransactions(): Transaction[] {
    return this.transactionStore.getAll();
  }
}

5. Use the Library Management System

app.ts

import { BookService } from "./book-service";
import { UserService } from "./user-service";
import { TransactionService } from "./transaction-service";

// Initialize Services
const bookService = new BookService();
const userService = new UserService();
const transactionService = new TransactionService();

// Add Books
bookService.addBook({ id: "1", title: "1984", author: "George Orwell", available: true });
bookService.addBook({ id: "2", title: "Brave New World", author: "Aldous Huxley", available: true });

// Add Users
userService.addUser({ id: "1", name: "Alice", email: "[email protected]" });
userService.addUser({ id: "2", name: "Bob", email: "[email protected]" });

// Issue Transactions
transactionService.addTransaction({ id: "1", bookId: "1", userId: "1", date: new Date() });

// Display Data
console.log("All Books:", bookService.getBooks());
console.log("Available Books:", bookService.getAvailableBooks());
console.log("All Users:", userService.getUsers());
console.log("Transactions:", transactionService.getTransactions());

6. Output

Executing: addBook with args: [{"id":"1","title":"1984","author":"George Orwell","available":true}]
Result: undefined
Executing: addBook with args: [{"id":"2","title":"Brave New World","author":"Aldous Huxley","available":true}]
Result: undefined
Executing: addUser with args: [{"id":"1","name":"Alice","email":"[email protected]"}]
Result: undefined
Executing: addUser with args: [{"id":"2","name":"Bob","email":"[email protected]"}]
Result: undefined
Executing: addTransaction with args: [{"id":"1","bookId":"1","userId":"1","date":"2023-12-21T10:00:00.000Z"}]
Result: undefined
Executing: getBooks with args: []
Result: [{"id":"1","title":"1984","author":"George Orwell","available":true},{"id":"2","title":"Brave New World","author":"Aldous Huxley","available":true}]

Key Features Demonstrated

  • Generics for reusable data stores.
  • Decorators for logging, validation, and metadata.
  • Interfaces for consistent type enforcement.
  • Abstract concepts like modularity and extensibility.

πŸ”¨πŸ€–πŸ”§ Let me know if you'd like additional features or enhancements for this example!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment