Transitioning to TypeScript: The Ultimate Starter Guide - Part 3

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:

// 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:

// 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:

// 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:

// 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:

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:

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:

// 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:

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:

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:

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:

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:

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! 💻✨

Did you find this article valuable?

Support Innovate Sphere by becoming a sponsor. Any amount is appreciated!