Transitioning to TypeScript: The Ultimate Starter Guide - Part 3
Tags: typescript, javascript, beginners, programming
Published: 2023-09-02 04:53:51

## TypeScript from JavaScript Part 3 of 7: Modules, Namespaces, and Decorators

Welcome back to the third part of our series on transitioning from JavaScript to TypeScript! In the last part, we delved deep into interfaces, classes, and generics in TypeScript. If you haven't read the previous parts, I would recommend going back and doing so as it will make understanding this part much easier.

In this part, we will focus on modules, namespaces, and decorators in TypeScript. These concepts are essential for organizing and structuring your code in a maintainable and scalable way.

## 1. Modules

Modules are a way to split your code into multiple files and manage dependencies between them. TypeScript, like JavaScript, has a module system that allows you to organize your code into separate files and import and export functions, variables, and types between them.

### a. Exporting and Importing

In TypeScript, you can export a function, variable, or type from a file using the `export` keyword, and then import it in another file using the `import` keyword.

Here is an example:

```typescript
// math.ts
export function add(x: number, y: number): number {
  return x + y;
}

// main.ts
import { add } from './math';

const result = add(1, 2);
console.log(result); // 3
```

In this example, the `add` function is exported from the `math.ts` file and then imported and used in the `main.ts` file.

You can also export and import classes, interfaces, and types:

```typescript
// point.ts
export interface Point {
  x: number;
  y: number;
}

// main.ts
import { Point } from './point';

const point: Point = { x: 1, y: 2 };
console.log(point); // { x: 1, y: 2 }
```

In this example, the `Point` interface is exported from the `point.ts` file and then imported and used in the `main.ts` file.

### b. Default Exports

Each module can have one default export. A default export can be a function, a class, or a variable.

Here is an example:

```typescript
// math.ts
export default function add(x: number, y: number): number {
  return x + y;
}

// main.ts
import add from './math';

const result = add(1, 2);
console.log(result); // 3
```

In this example, the `add` function is the default export of the `math.ts` file and is then imported and used in the `main.ts` file.

### c. Re-exporting

You can also re-export other modules from a module. This is useful when you want to create a single entry point for several modules.

Here is an example:

```typescript
// math.ts
export function add(x: number, y: number): number {
  return x + y;
}

// index.ts
export * from './math';

// main.ts
import { add } from './index';

const result = add(1, 2);
console.log(result); // 3
```

In this example, the `math.ts` file exports the `add` function, the `index.ts` file re-exports everything from the `math.ts` file, and the `main.ts` file imports the `add` function from the `index.ts` file.

## 2. Namespaces

Namespaces are a way to organize your code into logical groups and prevent naming conflicts. TypeScript uses the `namespace` keyword to define a namespace.

Here is an example:

```typescript
namespace Math {
  export function add(x: number, y: number): number {
    return x + y;
  }

  export function subtract(x: number, y: number): number {
    return x - y;
  }
}

const result = Math.add(1, 2);
console.log(result); // 3
```

In this example, the `add` and `subtract` functions are defined inside the `Math` namespace.

### a. Nested Namespaces

You can also nest namespaces inside other namespaces.

Here is an example:

```typescript
namespace Geometry {
  export namespace Circle {
    export function area(radius: number): number {
      return Math.PI * Math.pow(radius, 2);
    }
  }

  export namespace Square {
    export function area(side: number): number {
      return Math.pow(side, 2);
    }
  }
}

const circleArea = Geometry.Circle.area(5);
const squareArea = Geometry.Square.area(5);

console.log(circleArea); // 78.53981633974483
console.log(squareArea); // 25
```

In this example, the `Circle` and `Square` namespaces are defined inside the `Geometry` namespace.

### b. Splitting Namespaces Across Files

You can split a namespace across multiple files.

Here is an example:

```typescript
// circle.ts
namespace Geometry {
  export namespace Circle {
    export function area(radius: number): number {
      return Math.PI * Math.pow(radius, 2);
    }
  }
}

// square.ts
namespace

 Geometry {
  export namespace Square {
    export function area(side: number): number {
      return Math.pow(side, 2);
    }
  }
}

// main.ts
/// <reference path="circle.ts" />
/// <reference path="square.ts" />

const circleArea = Geometry.Circle.area(5);
const squareArea = Geometry.Square.area(5);

console.log(circleArea); // 78.53981633974483
console.log(squareArea); // 25
```

In this example, the `Geometry` namespace is split across the `circle.ts` and `square.ts` files, and then used in the `main.ts` file.

Note: While namespaces are still supported in TypeScript, they are considered outdated and modules are recommended for organizing your code.

## 3. Decorators

Decorators are a way to add metadata to your code and modify its behavior. TypeScript uses the `@` symbol to define a decorator.

Here is an example:

```typescript
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log('Arguments:', args);
    const result = originalMethod.apply(this, args);
    console.log('Result:', result);
    return result;
  };
}

class Math {
  @log
  static add(x: number, y: number): number {
    return x + y;
  }
}

const result = Math.add(1, 2);
// Arguments: [1, 2]
// Result: 3
```

In this example, the `log` decorator is used to log the arguments and the result of the `add` method of the `Math` class.

### a. Class Decorators

Class decorators are applied to the constructor of a class.

Here is an example:

```typescript
function sealed(constructor: Function): void {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return 'Hello, ' + this.greeting;
  }
}
```

In this example, the `sealed` decorator is used to prevent adding or removing properties from the `Greeter` class.

### b. Method Decorators

Method decorators are applied to the property descriptor of a method.

Here is an example:

```typescript
function enumerable(value: boolean): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return 'Hello, ' + this.greeting;
  }
}
```

In this example, the `enumerable` decorator is used to set the `enumerable` property of the `greet` method of the `Greeter` class.

### c. Property Decorators

Property decorators are applied to a property of a class.

Here is an example:

```typescript
function readonly(target: any, propertyKey: string): void {
  const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || { writable: true };
  descriptor.writable = false;
  Object.defineProperty(target, propertyKey, descriptor);
}

class Greeter {
  @readonly
  greeting: string = 'Hello, world!';
}
```

In this example, the `readonly` decorator is used to make the `greeting` property of the `Greeter` class read-only.

### d. Parameter Decorators

Parameter decorators are applied to a parameter of a method.

Here is an example:

```typescript
function required(target: any, propertyKey: string, parameterIndex: number): void {
  const existingRequiredParameters: number[] = Reflect.getMetadata('required', target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata('required', existingRequiredParameters, target, propertyKey);
}

class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet(@required name: string) {
    return 'Hello, ' + name + '! ' + this.greeting;
  }
}
```

In this example, the `required` decorator is used to mark the `name` parameter of the `greet` method of the `Greeter` class as required.

Note: The `Reflect.getMetadata` and `Reflect.defineMetadata` methods are part of the `reflect-metadata` library, which you need to install and import to use parameter decorators.

## Conclusion

In this part, we learned about modules, namespaces, and decorators in TypeScript. These concepts are essential for organizing and structuring your code in a maintainable and scalable way.

In the next part of this series, we will learn about advanced types and type guards in TypeScript. Stay tuned!

That's all for now, happy coding! 💻✨