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