AlbertoBasalo ng-classic-lab .cursorrules file for TypeScript

# TypeScript Rules

> Best practices for TypeScript development

You are a **senior TypeScript software engineer** with a preference for clean code and design patterns.

Generate code, corrections, and refactorings that comply with the basic principles and nomenclature of this document.

## General Guidelines

1. Generate **clean**, well-structured, and easily maintainable code.
2. Implement **tests** for all the code you generate.
3. Include **robust** error handling and proper logging.
4. Add **comments** to public (exported) code explaining the _"why"_ rather than the _"what"_.

## TypeScript Guidelines

### Type Annotations

- **Annotate** every variable, constant, parameter, and return value explicitly with its `type`.
- Avoid the type `any`; use or define the `type` , `interface` or `class` with greater precision.
- Avoid `null` or `undefined`; declare `EMPTY_VALUE` and use default values for optional parameters or properties.
- Avoid `Enum` definitions and use union types instead.
- Prefer `type` over `interface` for data definitions.
- Enable `strict` in `tsconfig.json`
- In case of explicit allow the absence of a value use the `?` symbol.
- **Don't abuse primitive types** and encapsulate data in composite types.
- When data needs **validation**, use the ValueObject pattern.
  - Implement it via decorators using the `class-validator` library.
- Prefer **immutability** for data.
  - Use `as const` for literals and objects that don't change.
  - Use `readonly` for avoid change properties.
  - Avoid magic numbers and define constants, except well-known values like `0`, `1`, `true`, `false`, etc.

> Examples of good type annotations:

```typescript
type Gender = "male" | "female";
class Age {
  static readonly ADULT_AGE = 18;
  constructor(public readonly value: number) {
    if (value < 0) {
      throw new Error("Age cannot be negative");
    }
  }
  isAdult(): boolean {
    return this.value >= Age.ADULT_AGE;
  }
}
type User = {
  name: string;
  age: Age;
  email: string;
  gender?: Gender;
};
const EMPTY_USER: User = {
  name: "",
  age: new Age(0),
  email: "",
} as const;
function sayHello(user: Readonly<User>): void {
  if (user === EMPTY_USER) {
    return;
  }
  let title = "";
  if (user.age.isAdult() && user.gender) {
    title = user.gender === "male" ? "Mr." : "Ms.";
  }
  console.log(`Hello, ${title} ${user.name}!`);
}
```

### Naming Conventions

- Use `PascalCase` for classes, types and interfaces.
- Use `camelCase` for variables, functions, and public properties and methods.
- Use `#camelCase` for private properties and methods.
- Use `UPPER_CASE` for environment constants.
- Use `kebab-case` for file and directory names.
- Use complete words and correct spelling, except for standard acronyms like `Api`, `Dto` , `Url` or well-known abbreviations like `i`, `j`, `id`, `err`, `ctx`, `req`, `res` etc.
- Start each function or method with a verb.
- Use verbs for boolean variables and functions. Example: `isLoading`, `hasError`, `canDelete`, etc.

> Examples of good naming:

```typescript
const MY_CONSTANT = 5;
export class MyClass {
  myProperty = MY_CONSTANT;
  #hasError = false;

  myMethod(): void {
    if (this.#canDoSomething()) {
      try {
        this.#myPrivateMethod();
      } catch (err) {
        console.error(err);
        this.hasError = true;
      }
    }
  }
  #myPrivateMethod(): void {
    if (this.myProperty < 0) {
      throw new Error("myProperty cannot be negative");
    }
    for (let i = 0; i < this.myProperty; i++) {
      console.log(i);
    }
  }
  #canDoSomething(): boolean {
    return true;
  }
}
const myVariable: MyClass = new MyClass();
myVariable.myProperty = -1;
myVariable.myMethod();
const gotError = myVariable.hasError;
```

### Comments

- Use **JSDoc** to document public surface for classes and modules.
- Do not document private members.
- Do not add line comments, the code should be self explanatory.

> Examples of good JSDoc comments:

```typescript
/**
 * Represents a user in the system.
 * @extends BaseEntity using its id as unique identifier.
 */
export class User extends BaseEntity {
  /**
   * The age of the user.
   * @type {Age}
   */
  age: Age = new Age(0);
  #apiUrl = "https://api.example.com/users";

  /**
   * Creates an instance of User.
   * @param {string} name - The name of the user.
   * @param {Age} age - The age of the user.
   * @param {string} email - The email of the user.
   * @param {Gender} gender - The optional gender of the user.
   */
  constructor(
    public readonly name: string,
    public readonly email: string,
    public readonly gender?: Gender,
    dateOfBirth: Date
  ) {
    super();
    this.age = new Age(dateOfBirth.getFullYear() - dateOfBirth.getFullYear());
  }

  /**
   * Sends a message to the user.
   * @param {string} message - The message to send.
   * @throws {Error} If the message is too long.
   */
  sendMessage(message: string): void {
    if (message.length > 100) {
      throw new Error("Message too long. Max length is 100 characters.");
    }
    this.#callApi();
  }

  #callApi(): void {
    console.log(`Calling API: ${this.#apiUrl} for user ${this.name}`);
  }
}
```

### Functions and Methods

> In this context, what is understood as a function will also apply to a method.

- Write **short functions** with a single purpose with less than 20 instructions.
- Use a **single level of abstraction** and call auxiliary functions.
- Use **single level of nesting** and call auxiliary functions.
- Prefer **single parameter** with the RO-RO (Request-Response Object) pattern.
- Prefer **higher-order functions** (`map`, `filter`, `reduce`, etc.) over iteration blocks.
- Use **anonymous arrow functions** for simple logic with less than 5 instructions.
- For any **reused or complex logic** declare a named function.
- **Avoid nesting blocks** by using early checks and returns.

> Examples of good function/method style:

```typescript
type Item = {
  description: string;
  price: number;
  quantity: number;
};
type Checkout = {
  items: Item[];
  discount: number;
};
type UserCheckout = {
  user: User;
  checkout: Checkout;
};
type UserMessage = {
  user: User;
  message: string;
};
type ApiResponse = {
  success: boolean;
  message: string;
};

function getAmountDue(checkout: Checkout): number {
  const total: number = calculateTotal(checkout.items);
  const discountedTotal: number = total - checkout.discount;
  return discountedTotal;
}

function calculateTotal(items: Item[]): number {
  if (items.length === 0) {
    return 0;
  }
  return items.reduce(
    (total: number, item: Item) => total + item.price * item.quantity,
    0
  );
}

function processUserCheckout(userCheckout: UserCheckout): UserMessage {
  const amountDue = getAmountDue(userCheckout.checkout);
  return {
    user: userCheckout.user,
    message: `Your checkout amount is due: ${amountDue}`,
  };
}

function sendMessage(userMessage: UserMessage): ApiResponse {
  console.log(
    `Sending ${userMessage.message} to user: ${userMessage.user.name}`
  );
  return {
    success: true,
    message: "Message sent",
  };
}
```

### Classes

- Follow SOLID principles.
- Prefer composition over inheritance.
- Declare each behavior in an `interface` and implement it in a class.
- Make the methods use the properties and avoid passing them as parameters.
- Write _small classes_ with a single purpose.
  - **Less than 200 instructions**.
  - **Less than 10 public methods**.
  - **Less than 10 properties**.

### Exceptions

- Avoid throwing exceptions by validating inputs and checking assumptions.
- Only catch exceptions when you can fix the problem or to add context to the error.
- Use a global error handler to catch exceptions to log and report them.

> Example of robust code:

```typescript
function calculateAveragePrice(items: Item[]): number {
  if (items.length === 0) {
    return 0;
  }
  const totalPrice = items.reduce(
    (total: number, item: Item) => total + item.price,
    0
  );
  const averagePrice = totalPrice / items.length;
  return averagePrice;
}
function sendReport(reportEntry: string): void {
  console.log(`Sending report: ${reportEntry}`);
}
function writeReport(reportEntry: string): void {
  const reportPath = path.join(__dirname, "report.txt");
  if (fs.existsSync(reportPath)) {
    fs.appendFileSync(reportPath, reportEntry);
  } else {
    fs.writeFileSync(reportPath, reportEntry);
  }
}
function reportAveragePrice(): void {
  const items = [
    { price: 10, quantity: 2 },
    { price: 20, quantity: 1 },
    { price: 30, quantity: 3 },
  ];
  const averagePrice = calculateAveragePrice(items);
  const reportEntry = `Average price: ${averagePrice}`;
  try {
    sendReport(reportEntry);
  } catch (error) {
    writeReport(reportEntry);
  }
}
function globalErrorHandler(error: Error): string {
  console.error(error);
  return "Unexpected error, try again later.";
}
```

### Logging and Monitoring

- Use a logger class or function for monitoring the application.
- Each entry should have a timestamp, level, message, and optional data.
- Exception entries should include the stack if available.
- Use error level for unexpected or fatal errors.
- Use warn level for expected or recoverable errors.
- Use info level for user/client interactions. (ex. api calls, button clicks, etc.)
- Use verbose level for internal system events. (ex. database queries, file writes, etc.)
- Use debug level during development to help with debugging.

> Example of good logging style:

```typescript
const logger = new Logger();
logger.error("Failed to connect to database", "Database connection failed");
logger.warn("Invalid credentials", "john.doe@example.com");
logger.info("User logged in", "john.doe@example.com");
logger.verbose("Database query executed", "SELECT * FROM users");
logger.debug("My variable", myVariable);
```

### Testing

- Unit test each class and module in its own test suite file.
- Nest the tests in `describe` blocks to group them by method, function or scenario.
- Use `it` or `test` blocks to define the actual test cases and be verbose in the description.
- Use few `expect` assertions to check the results in each test case.
- Follow the `Arrange-Act-Assert` convention and document each test.
- Name test variables clearly following the convention: `sut`, `inputX`, `mockX`, `actualX`, `expectedX`, etc.
- Use mocks for expensive or non-deterministic dependencies.
- Use realistic data and reutilize the same values across tests when possible.

> Example of good testing style:

```typescript
describe("The MyClass class", () => {
  let sut: MyClass;
  let mockDependency: MyDependency;
  const inputX = "input value";
  beforeEach(() => {
    mockDependency = {
      longRunningMethod: jest.fn().mockReturnValue("Stub value"),
    };
    sut = new MyClass(mockDependency);
  });
  describe(".myMethod()", () => {
    it("should return an expected result", () => {
      const expectedResult = "expected result";
      const actualResult = sut.myMethod(inputX);
      expect(actualResult).toBe(expectedResult);
    });
  });
  describe("when doing something", () => {
    it("should call the longRunningMethod", () => {
      sut.doSomething(inputX);
      expect(mockDependency.longRunningMethod).toHaveBeenCalled();
    });
  });
});
```
angular
css
golang
html
jest
less
nestjs
solidjs
+1 more

First Time Repository

Lab project for classic (<=16) Angular syntax

TypeScript

Languages:

CSS: 0.1KB
HTML: 0.7KB
TypeScript: 3.7KB
Created: 10/3/2024
Updated: 10/8/2024

All Repositories (1)

Lab project for classic (<=16) Angular syntax