# 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