Typescript


TypeScript Beginners To Experts


The site is still under development.

Basics

1.1: Introduction to TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It adds optional static typing to JavaScript, making it more robust and scalable for large applications.

  • TypeScript provides additional features over JavaScript, such as type annotations, interfaces, and classes.
  • It is widely used in large-scale applications for better maintainability and fewer bugs.

1.2: Setting up your Environment

To get started with TypeScript, you need to set up Node.js and the TypeScript compiler. Here's how:

  • Install Node.js and npm (Node Package Manager) from Node.js.
  • Install TypeScript globally using npm:
npm install -g typescript

After installation, verify the TypeScript compiler with:

tsc --version

1.3: Basic TypeScript Syntax

Here are some basic TypeScript syntax examples:

Variables and Data Types

let num: number = 10;
let name: string = "John";
let isActive: boolean = true;

Basic Operators

let sum = 5 + 3;
let isEqual = (5 === 5);

Control Flow (if-else)

if (num > 5) { console.log("Greater"); }
else { console.log("Smaller or Equal"); }

1.4: Compiling and Running TypeScript

To compile TypeScript to JavaScript, follow these steps:

1. Create a file called app.ts with the following content:

let greeting: string = "Hello, TypeScript!";

2. Compile it using the tsc command:

tsc app.ts

3. This will generate a app.js file. You can run it using:

node app.js

2.1: Explicit Types

In TypeScript, we can explicitly define the types of variables. Below are examples of type annotations, type inference, and working with primitive types.

Type Annotations

let age: number = 25;

Output: 25

Description: We explicitly define the variable age as a number.

Type Inference

let name = "Alice"; // TypeScript infers the type as string

Output: Alice

Description: TypeScript automatically infers that name is of type string based on its assigned value.

Working with Primitive Types

let isActive: boolean = true;

Output: true

Description: We explicitly define the variable isActive as a boolean and assign it a value of true.


2.2: Arrays and Tuples

Arrays and Tuples in TypeScript allow us to store multiple values of a specific type or different types, respectively.

Array Type Annotations

let numbers: number[] = [1, 2, 3, 4];

Output: [1, 2, 3, 4]

Description: We define an array numbers that can only hold number values.

Tuple Types

let person: [string, number] = ["Alice", 30];

Output: ["Alice", 30]

Description: We define a tuple person that contains a string and a number in a fixed order.

Array Manipulation

let numbers: number[] = [1, 2, 3];
numbers.push(4);

Output: [1, 2, 3, 4]

Description: We add a new element to the numbers array using the push() method.


2.3: Objects and Interfaces

Objects and interfaces help in defining the structure of complex data. Below are examples of object type annotations, defining interfaces, and optional properties.

Object Type Annotations

let person: { name: string, age: number } = { name: "John", age: 25 };

Output: { name: "John", age: 25 }

Description: We define an object person with specific properties: name (string) and age (number).

Defining Interfaces

interface Person { name: string; age: number; }
let person: Person = { name: "John", age: 25 };

Output: { name: "John", age: 25 }

Description: We define an interface Person to describe the structure of the object person.

Optional Properties

interface Person { name: string; age?: number; }
let person: Person = { name: "John" };

Output: { name: "John" }

Description: The age property is optional in the Person interface, so it can be omitted.


2.4: Type Aliases and Union Types

Type aliases allow us to create custom types, while union types allow us to combine multiple types. Below are examples of both:

Creating Type Aliases

type ID = string | number;
let userId: ID = 101;

Output: 101

Description: We create a type alias ID that can be either a string or a number.

Union Types

let id: string | number = "A123";

Output: A123

Description: We define a variable id that can either be a string or a number.

Type Guards

function printId(id: string | number) {
  if (typeof id === "string") {
    console.log("String ID:", id);
  } else {
    console.log("Number ID:", id);
  }
}
printId(123);

Output: Number ID: 123

Description: A type guard checks the type of the variable id to ensure that it is either a string or number before performing an action.


2.5: Enums

Enums allow us to define a set of named constants. Below are examples of numeric, string, and constant enums:

Numeric Enums

enum Direction { Up, Down, Left, Right }
let move: Direction = Direction.Up;

Output: 0

Description: By default, numeric enums start from 0. The Direction.Up is assigned the value 0.

String Enums

enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
let move: Direction = Direction.Left;

Output: LEFT

Description: String enums allow us to assign specific string values to each enum member.

Constant Enums

const enum Direction { Up, Down, Left, Right }
let move: Direction = Direction.Right;

Output: 3

Description: Constant enums are inlined at compile-time, making them more efficient.

3.1: Function Types

Functions in TypeScript can have type annotations for parameters and return values. Below are examples of function type annotations, optional and default parameters, and rest parameters.

Function Type Annotations

function greet(name: string): string {
    return "Hello, " + name;
}

Output: Hello, Alice

Description: We define a function greet that takes a string parameter and returns a string.

Optional Parameters

function greet(name: string, age?: number): string {
    if (age) {
        return "Hello, " + name + ". You are " + age + " years old.";
    }
    return "Hello, " + name;
}

Output: Hello, Alice. You are 30 years old.

Description: The age parameter is optional in the greet function.

Default Parameters

function greet(name: string, greeting: string = "Hello"): string {
    return greeting + ", " + name;
}

Output: Hello, Alice

Description: The greeting parameter has a default value of "Hello" if not provided.


3.2: Function Overloading

Function overloading allows us to define multiple signatures for the same function. Below is an example of overloading.

Function Overloading

function greet(name: string): string;
function greet(name: string, age: number): string;
function greet(name: string, age?: number): string {
    if (age) {
        return "Hello, " + name + ". You are " + age + " years old.";
    }
    return "Hello, " + name;
}

Output: Hello, Alice. You are 30 years old.

Description: We overload the greet function to allow both one parameter or two parameters (name and age).


3.3: Classes

Classes in TypeScript are used to define blueprints for creating objects. Below are examples of defining classes and using constructors and access modifiers.

Defining a Class

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

let person = new Person("Alice", 30);

Output: Person { name: "Alice", age: 30 }

Description: We define a class Person with a constructor to initialize the properties name and age.

Access Modifiers

class Person {
    public name: string;
    private age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    getAge() {
        return this.age;
    }
}

let person = new Person("Alice", 30);
console.log(person.getAge());

Output: 30

Description: The age property is private, but we can access it using the getAge method.

Constructor Method

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

let person = new Person("Bob", 25);
console.log(person.name); // Bob

Output: Bob

Description: The constructor method is called when creating an instance of the Person class.


3.4: Inheritance and Polymorphism

Inheritance allows us to extend the functionality of a class, and polymorphism lets us use the same method name in different classes. Below are examples of inheritance and polymorphism.

Extending Classes

class Animal {
    makeSound(): string {
        return "Some sound";
    }
}

class Dog extends Animal {
    makeSound(): string {
        return "Woof!";
    }
}

let dog = new Dog();
console.log(dog.makeSound());

Output: Woof!

Description: The Dog class inherits from Animal and overrides the makeSound method.

Method Overriding

class Animal {
    makeSound(): string {
        return "Some sound";
    }
}

class Dog extends Animal {
    makeSound(): string {
        return "Woof!";
    }
}

let animal: Animal = new Dog();
console.log(animal.makeSound());

Output: Woof!

Description: We override the makeSound method in the Dog class. Polymorphism allows us to call the method on the base class type.

Abstract Classes

abstract class Animal {
    abstract makeSound(): string;
}

class Dog extends Animal {
    makeSound(): string {
        return "Woof!";
    }
}

let dog = new Dog();
console.log(dog.makeSound());

Output: Woof!

Description: We define an abstract class Animal with an abstract method makeSound, which must be implemented by subclasses.


3.5: Getters and Setters

Getters and setters allow us to control access to the properties of a class. Below is an example of defining and using getters and setters.

Getters and Setters

class Person {
    private _age: number;

    constructor(age: number) {
        this._age = age;
    }

    get age(): number {
        return this._age;
    }

    set age(age: number) {
        if (age < 0) {
            throw new Error("Age cannot be negative");
        }
        this._age = age;
    }
}

let person = new Person(25);
person.age = 30; // setter
console.log(person.age); // getter

Output: 30

Description: The getter and setter methods allow controlled access to the _age property.

4.1: Generic Functions

Generics allow us to create reusable and flexible functions that work with different data types. Below are examples of creating and using generic functions.

Generic Function Example

function identity(arg: T): T {
    return arg;
}

let output = identity(5); // number
let output2 = identity("Hello"); // string

Output: 5, Hello

Description: We define a generic function identity that works with any type T.

Generic Function with Constraints

function getLength(arg: T): number {
    return arg.length;
}

let length = getLength([1, 2, 3]); // 3
let length2 = getLength("Hello"); // 5

Output: 3, 5

Description: The function getLength only accepts arguments that have a length property, like arrays and strings.


4.2: Generic Classes and Interfaces

Generics can also be applied to classes and interfaces. Below are examples of generic classes and interfaces in TypeScript.

Generic Class

class Box {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

let box = new Box(123);
console.log(box.value); // 123

Output: 123

Description: The Box class is a generic class that can store any type of value. In this case, it stores a number.

Generic Interface

interface Pair {
    key: K;
    value: V;
}

let pair: Pair = { key: "age", value: 25 };
console.log(pair.key, pair.value); // age 25

Output: age 25

Description: The Pair interface is a generic interface that defines a key-value pair with types K and V.


4.3: Utility Types

TypeScript provides several utility types to facilitate common type transformations. Below are some examples.

Partial Type

interface Person {
    name: string;
    age: number;
}

let partialPerson: Partial = { name: "Alice" }; // name is optional
console.log(partialPerson); // { name: "Alice" }

Output: { name: "Alice" }

Description: The Partial utility type makes all properties of Person optional.

Readonly Type

interface Person {
    name: string;
    age: number;
}

let readonlyPerson: Readonly = { name: "Alice", age: 30 };
readonlyPerson.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property.

Output: Error (because name is readonly)

Description: The Readonly utility type makes all properties of Person read-only.


4.4: Type Guards and Type Assertions

Type guards help to narrow down the type within conditional blocks, and type assertions allow you to assert the type of a variable.

Type Guard Example

function isString(value: any): value is string {
    return typeof value === "string";
}

let value: any = "Hello";
if (isString(value)) {
    console.log(value.toUpperCase()); // HELLO
}

Output: HELLO

Description: The isString function acts as a type guard, ensuring that value is a string.

Type Assertion Example

let value: any = "Hello, World!";
let length = (value as string).length;
console.log(length); // 13

Output: 13

Description: We use a type assertion (value as string) to tell TypeScript that value is a string.


4.5: Mapped Types and Template Literal Types

Mapped types allow us to create new types by transforming an existing type, and template literal types allow us to construct types using string literals.

Mapped Types Example

type Person = {
    name: string;
    age: number;
};

type ReadOnlyPerson = {
    readonly [K in keyof Person]: Person[K];
}

let person: ReadOnlyPerson = { name: "Alice", age: 30 };
person.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property.

Output: Error (name is readonly)

Description: The ReadOnlyPerson type is created using a mapped type, making all properties read-only.

Template Literal Types Example

type Greeting = `Hello, ${string}`;

let greet: Greeting = "Hello, World!"; // Correct
let greet2: Greeting = "Hi, World!"; // Error

Output: Hello, World!

Description: The Greeting type allows any string that starts with "Hello, ". Other strings like "Hi, World!" will cause an error.

5.1: Modules

Modules in TypeScript allow us to break down our code into smaller, reusable pieces. Below are examples of working with modules in TypeScript.

Exporting and Importing Modules

// In file mathFunctions.ts
export function add(a: number, b: number): number {
    return a + b;
}

export function subtract(a: number, b: number): number {
    return a - b;
}

// In file main.ts
import { add, subtract } from './mathFunctions';

console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

Output: 8, 2

Description: In this example, we are exporting two functions add and subtract from the mathFunctions.ts file, and importing them in main.ts to use them.

Default Exports

// In file logger.ts
export default function logMessage(message: string): void {
    console.log(message);
}

// In file main.ts
import logMessage from './logger';

logMessage("Hello, World!"); // Hello, World!

Output: Hello, World!

Description: This example demonstrates the use of default exports. The logMessage function is exported as the default from logger.ts, and imported using any name in main.ts.


5.2: Namespaces (Legacy)

Namespaces are an older way to organize code in TypeScript. They allow you to group related functionality together. Below is an example of defining and using namespaces.

Defining a Namespace

namespace MathFunctions {
    export function add(a: number, b: number): number {
        return a + b;
    }

    export function subtract(a: number, b: number): number {
        return a - b;
    }
}

console.log(MathFunctions.add(5, 3)); // 8
console.log(MathFunctions.subtract(5, 3)); // 2

Output: 8, 2

Description: Here, we define a MathFunctions namespace that contains the add and subtract functions. We can access them by referencing the namespace name.

Nested Namespaces

namespace MathFunctions {
    export namespace Advanced {
        export function multiply(a: number, b: number): number {
            return a * b;
        }
    }
}

console.log(MathFunctions.Advanced.multiply(5, 3)); // 15

Output: 15

Description: This example demonstrates nested namespaces. The multiply function is defined inside the nested Advanced namespace within the MathFunctions namespace.


5.3: Module Resolution

TypeScript uses module resolution strategies to locate and include external modules. The module resolution strategy can be configured in the tsconfig.json file.

Configuring Module Resolution

// tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "moduleResolution": "node"
    }
}

Description: The moduleResolution option is set to node, which mimics Node.js module resolution. This tells TypeScript to look for modules in node_modules and resolve them based on their paths.


5.4: Working with Libraries

When working with external libraries, TypeScript provides a way to include type definitions to help with development. You can install type definitions from @types packages.

Importing from Libraries

// In this example, we're using the lodash library

import _ from 'lodash';

const result = _.chunk([1, 2, 3, 4, 5, 6], 2);
console.log(result); // [[1, 2], [3, 4], [5, 6]]

Output: [[1, 2], [3, 4], [5, 6]]

Description: This example shows how to import and use an external library (in this case, lodash) with TypeScript. We also benefit from type definitions if the library provides them.

Installing Type Definitions

// To install type definitions for lodash, run:
// npm install --save-dev @types/lodash

Description: To ensure TypeScript knows the types of an external library, you can install type definitions. This allows TypeScript to offer type checking and auto-completion while using the library.

6.1: Promises

Promises are used to handle asynchronous operations. Below are examples demonstrating how to work with promises in TypeScript.

Creating and Using Promises

function fetchData(): Promise {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data fetched successfully!");
        }, 2000);
    });
}

fetchData().then(response => {
    console.log(response); // Data fetched successfully!
}).catch(error => {
    console.error(error);
});

Output: Data fetched successfully!

Description: This example creates a promise with a simulated asynchronous operation (using setTimeout). Once the promise resolves, the then handler prints the response.

Error Handling with Promises

function fetchDataWithError(): Promise {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("Error fetching data!");
        }, 2000);
    });
}

fetchDataWithError().then(response => {
    console.log(response);
}).catch(error => {
    console.error(error); // Error fetching data!
});

Output: Error fetching data!

Description: This example shows how to handle errors with the catch method. The promise rejects, and the error is caught and logged.


6.2: Asynchronous Functions

Asynchronous functions simplify working with promises using the async and await keywords. Here are examples of asynchronous functions.

Creating Asynchronous Functions

async function fetchDataAsync(): Promise {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const data = await response.json();
    return `Fetched title: ${data.title}`;
}

fetchDataAsync().then(result => {
    console.log(result); // Fetched title: Some title
}).catch(error => {
    console.error(error);
});

Output: Fetched title: Some title

Description: This example shows how to define an asynchronous function using async. The await keyword is used to wait for the promise returned by fetch to resolve.

Handling Errors with Async/Await

async function fetchDataWithErrorAsync(): Promise {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/invalid-url');
        const data = await response.json();
        return `Fetched data: ${data}`;
    } catch (error) {
        throw new Error("Error fetching data");
    }
}

fetchDataWithErrorAsync().then(result => {
    console.log(result);
}).catch(error => {
    console.error(error.message); // Error fetching data
});

Output: Error fetching data

Description: This example shows how to handle errors within asynchronous functions using try and catch blocks. If the fetch fails, an error is thrown and caught in the catch block.


6.3: Working with Observables (RxJS)

Observables are another way to handle asynchronous operations, primarily used in reactive programming. Below is an example of working with observables using the RxJS library.

Creating an Observable

import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {
    setTimeout(() => {
        subscriber.next("Hello from Observable!");
        subscriber.complete();
    }, 2000);
});

observable.subscribe({
    next(value) {
        console.log(value); // Hello from Observable!
    },
    complete() {
        console.log("Observable completed");
    }
});

Output: Hello from Observable!
Observable completed

Description: In this example, an observable is created that emits a value after a 2-second delay. The subscribe method is used to listen for the emitted value and when the observable completes.

Using Operators with Observables

import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

const observable = new Observable(subscriber => {
    subscriber.next(5);
    subscriber.next(10);
    subscriber.complete();
});

observable.pipe(
    map(value => value * 2)
).subscribe({
    next(value) {
        console.log(value); // 10, 20
    },
    complete() {
        console.log("Observable completed");
    }
});

Output: 10, 20
Observable completed

Description: This example shows how to use the map operator with observables. The emitted values are multiplied by 2 before being logged in the subscribe method.

7.1: Decorators

Decorators are special types of declarations that can be attached to classes, methods, properties, or parameters. Here are examples to demonstrate how decorators work in TypeScript.

Class Decorator

function classDecorator(constructor: Function) {
    console.log("Class Decorator called");
}

@classDecorator
class MyClass {
    constructor() {
        console.log("MyClass instance created");
    }
}

const myClass = new MyClass();

Output: Class Decorator called
MyClass instance created

Description: This example shows how to define a class decorator. The decorator logs a message when the class is defined. The constructor logs another message when the class is instantiated.

Method Decorator

function logMethod(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Method ${propertyName} called with arguments: ${args}`);
        return originalMethod.apply(this, args);
    };
}

class MyClass {
    @logMethod
    greet(name: string) {
        console.log(`Hello, ${name}`);
    }
}

const obj = new MyClass();
obj.greet("John");

Output: Method greet called with arguments: John
Hello, John

Description: The method decorator is used to modify the behavior of the method. Here, the decorator logs the arguments before calling the original method.


7.2: Metadata Reflection

Metadata reflection is a way to store and retrieve metadata about classes, methods, or properties. This requires the reflect-metadata library.

Using reflect-metadata

import "reflect-metadata";

function metadataDecorator(target: any, propertyKey: string) {
    Reflect.defineMetadata("customMetadata", "Some custom metadata", target, propertyKey);
}

class MyClass {
    @metadataDecorator
    myProperty: string;
}

const metadata = Reflect.getMetadata("customMetadata", MyClass.prototype, "myProperty");
console.log(metadata); // Some custom metadata

Output: Some custom metadata

Description: This example demonstrates how to use the reflect-metadata library to attach metadata to a property and retrieve it.

Accessing Metadata with Reflect

import "reflect-metadata";

function classMetadata(target: Function) {
    Reflect.defineMetadata("classMetadata", "Class metadata example", target);
}

@classMetadata
class MyClass {}

const metadata = Reflect.getMetadata("classMetadata", MyClass);
console.log(metadata); // Class metadata example

Output: Class metadata example

Description: In this example, metadata is added to a class, and we retrieve it using the Reflect.getMetadata method.


7.3: Custom Decorators

Custom decorators allow you to define your own decorator logic to reuse throughout your application. Below is an example of a custom decorator factory.

Creating a Custom Decorator Factory

function logParameter(target: any, methodName: string, parameterIndex: number) {
    const existingRequiredParameters: number[] = Reflect.getOwnMetadata("design:paramtypes", target, methodName) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata("design:paramtypes", existingRequiredParameters, target, methodName);
}

class MyClass {
    greet(@logParameter name: string) {
        console.log(`Hello, ${name}`);
    }
}

const obj = new MyClass();
obj.greet("John");

Output: Hello, John

Description: This example shows how to create a custom decorator factory that can be used to modify parameters of methods in TypeScript. It uses metadata reflection to track parameter types.

Combining Multiple Decorators

function decoratorA(target: any) {
    console.log("Decorator A");
}

function decoratorB(target: any) {
    console.log("Decorator B");
}

@decoratorA
@decoratorB
class MyClass {}

const obj = new MyClass();

Output: Decorator B
Decorator A

Description: In this example, two decorators are combined on a class. The decorators are applied in reverse order (bottom to top).

8.1: Unit Testing with Jest/Mocha

Unit testing is the process of testing individual components of your code to ensure they function as expected. Below is an example using Jest.

Unit Test with Jest

// math.ts
export function add(a: number, b: number): number {
    return a + b;
}

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

test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
});

Output: Test passes if the result is correct.

Description: This example shows a simple test case for the `add` function using Jest. The test checks if the sum of 1 and 2 equals 3.

Mocking Functions with Jest

// logger.ts
export function log(message: string) {
    console.log(message);
}

// logger.test.ts
import { log } from './logger';

test('should call log function', () => {
    const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
    log('Test message');
    expect(logMock).toHaveBeenCalledWith('Test message');
    logMock.mockRestore();
});

Output: Verifies that the `log` function is called with the correct message.

Description: This example shows how to mock functions in Jest. It mocks `console.log` to verify that it's called with a specific message.


8.2: Debugging TypeScript

Debugging helps identify and resolve errors in your code. Below is an example of debugging TypeScript in VS Code.

Debugging with VS Code

// debug.ts
function multiply(a: number, b: number): number {
    return a * b;
}

const result = multiply(3, 4);
console.log(result); // Set a breakpoint here to debug

Output: The multiplication result will be logged in the console.

Description: In VS Code, you can set a breakpoint in the code by clicking next to the line number. This allows you to inspect variables and step through the code during execution.

Using Source Maps for Debugging

// tsconfig.json
{
    "compilerOptions": {
        "sourceMap": true
    }
}

Output: Source maps enable debugging of TypeScript directly in the browser or VS Code.

Description: Setting `"sourceMap": true` in the `tsconfig.json` file enables source maps, allowing you to debug TypeScript code directly instead of the compiled JavaScript.


8.3: Linting with ESLint

Linter tools help enforce coding standards and identify potential errors. Below is an example of setting up ESLint for TypeScript.

Setting up ESLint with TypeScript

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Output: Install ESLint and TypeScript ESLint plugins for linting TypeScript code.

Description: This command installs the necessary ESLint dependencies for linting TypeScript files.

Configuring ESLint for TypeScript

// .eslintrc.json
{
    "parser": "@typescript-eslint/parser",
    "plugins": ["@typescript-eslint"],
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended"
    ]
}

Output: Configures ESLint to use the TypeScript parser and recommended TypeScript rules.

Description: This example shows how to configure ESLint to work with TypeScript. It extends the default ESLint recommended rules with TypeScript-specific rules.

Fixing Linting Errors

// bad-code.ts
const greet = (name) => {
    console.log(`Hello, ${name}`);
};

Output: ESLint will flag an error because the parameter is missing a type annotation.

Description: ESLint will flag this code for missing type annotations. To fix it, add a type to the `name` parameter like `name: string`.

9.1: Design Patterns in TypeScript

Design patterns are reusable solutions to common problems that occur in software design. Below are examples of implementing a few design patterns in TypeScript.

Singleton Pattern

class Singleton {
    private static instance: Singleton;

    private constructor() {}

    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true

Output: true

Description: This example demonstrates the Singleton pattern, ensuring only one instance of the class can exist.

Factory Pattern

class Car {
    constructor(public make: string, public model: string) {}
}

class CarFactory {
    static createCar(make: string, model: string): Car {
        return new Car(make, model);
    }
}

const myCar = CarFactory.createCar('Toyota', 'Camry');
console.log(myCar); // Car { make: 'Toyota', model: 'Camry' }

Output: Car { make: 'Toyota', model: 'Camry' }

Description: The Factory pattern allows the creation of objects without specifying the exact class of the object that will be created.

Design patterns in TypeScript can be extended to handle more complex scenarios. Below are some advanced examples of design patterns.

Observer Pattern

interface Observer {
    update(message: string): void;
}

class Subject {
    private observers: Observer[] = [];

    addObserver(observer: Observer) {
        this.observers.push(observer);
    }

    removeObserver(observer: Observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }

    notifyObservers(message: string) {
        this.observers.forEach(observer => observer.update(message));
    }
}

class ConcreteObserver implements Observer {
    constructor(private name: string) {}
    update(message: string) {
        console.log(`${this.name} received message: ${message}`);
    }
}

const subject = new Subject();
const observer1 = new ConcreteObserver('Observer 1');
const observer2 = new ConcreteObserver('Observer 2');

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers('New message');

Output: Observer 1 received message: New message
Observer 2 received message: New message

Description: The Observer pattern is used for creating a subscription mechanism to notify multiple objects of state changes. It's useful for building event-driven architectures.

Decorator Pattern

interface Coffee {
    cost(): number;
}

class SimpleCoffee implements Coffee {
    cost(): number {
        return 5;
    }
}

class MilkDecorator implements Coffee {
    constructor(private coffee: Coffee) {}

    cost(): number {
        return this.coffee.cost() + 2;
    }
}

class SugarDecorator implements Coffee {
    constructor(private coffee: Coffee) {}

    cost(): number {
        return this.coffee.cost() + 1;
    }
}

const coffee = new SimpleCoffee();
console.log(coffee.cost()); // 5

const milkCoffee = new MilkDecorator(coffee);
console.log(milkCoffee.cost()); // 7

const milkSugarCoffee = new SugarDecorator(milkCoffee);
console.log(milkSugarCoffee.cost()); // 8

Output: 5
7
8

Description: The Decorator pattern dynamically adds behavior to an object. It helps extend the functionality of classes without modifying their structure.


9.2: Performance Optimization

Performance optimization is critical to ensure the efficiency of your TypeScript applications. Below are examples to optimize TypeScript code.

Avoiding `any`

let userData: { name: string; age: number } = { name: 'John', age: 30 };
userData = { name: 'Jane', age: 25 }; // Type-safe

Output: Code is type-safe without using `any`.

Description: Avoid using the `any` type, as it bypasses TypeScript's static typing, leading to potential runtime errors.

Optimizing Type Inference

let arr = [1, 2, 3]; // Inferred as number[]
arr.push(4);
console.log(arr); // [1, 2, 3, 4]

Output: [1, 2, 3, 4]

Description: TypeScript infers types based on the array elements, reducing the need to explicitly annotate types.

In advanced TypeScript applications, optimizing performance is crucial for handling large data sets or improving application speed. Below are advanced performance techniques.

Memoization

function memoize(fn: Function) {
    const cache: { [key: string]: any } = {};
    return (...args: any[]) => {
        const key = JSON.stringify(args);
        if (cache[key]) {
            return cache[key];
        }
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

const slowFunction = (x: number) => {
    console.log('Running slow function...');
    return x * 2;
};

const memoizedSlowFunction = memoize(slowFunction);

console.log(memoizedSlowFunction(2)); // Running slow function... 4
console.log(memoizedSlowFunction(2)); // 4 (cached)

Output: Running slow function... 4
4 (cached)

Description: Memoization is an optimization technique that caches the result of expensive function calls and returns the cached result when the same inputs occur again.

Lazy Loading

class DataFetcher {
    private data: string[];

    constructor() {
        this.data = [];
    }

    fetchData(): string[] {
        if (this.data.length === 0) {
            console.log('Fetching data...');
            this.data = ['Item1', 'Item2', 'Item3'];
        }
        return this.data;
    }
}

const fetcher = new DataFetcher();
console.log(fetcher.fetchData()); // Fetching data... ['Item1', 'Item2', 'Item3']
console.log(fetcher.fetchData()); // ['Item1', 'Item2', 'Item3']

Output: Fetching data... ['Item1', 'Item2', 'Item3']
['Item1', 'Item2', 'Item3']

Description: Lazy loading ensures data is only fetched when necessary, thus saving resources and improving performance by delaying execution until the data is needed.


9.3: Code Organization and Architecture

Good code organization and architecture help maintainable and scalable applications. Below are examples of organizing TypeScript projects using best practices.

Layered Architecture

// Controller.ts
class UserController {
    constructor(private userService: UserService) {}

    createUser() {
        this.userService.save();
    }
}

// Service.ts
class UserService {
    save() {
        console.log('User saved');
    }
}

const userService = new UserService();
const userController = new UserController(userService);
userController.createUser();

Output: User saved

Description: This example shows a basic implementation of a layered architecture, separating concerns between the controller and service layers.

Clean Code Principles

function calculateArea(radius: number): number {
    return Math.PI * radius * radius;
}

const area = calculateArea(5);
console.log(area); // 78.53981633974483

Output: 78.53981633974483

Description: This example follows clean code principles by using a simple, self-explanatory function to calculate the area of a circle.


9.4: Building Libraries and Frameworks

Building reusable libraries and frameworks can help you and others streamline development. Below is an example of how you could create a small TypeScript utility library.

Building a Utility Library

// util.ts
export function sum(a: number, b: number): number {
    return a + b;
}

// index.ts
import { sum } from './util';
console.log(sum(1, 2)); // 3

Output: 3

Description: This example demonstrates how to create a simple utility function and export it as part of a library.

Creating a Reusable Framework

class Logger {
    static log(message: string) {
        console.log(`[LOG]: ${message}`);
    }
}

Logger.log('This is a message');

Output: [LOG]: This is a message

Description: This example shows how to create a reusable framework for logging messages. It defines a static method on a class, allowing it to be used globally.


9.5: TypeScript and Frontend Frameworks (React, Angular, Vue)

Integrating TypeScript with frontend frameworks ensures you can leverage static typing while building your application. Below are examples of how TypeScript works with popular frontend frameworks.

TypeScript with React

import React from 'react';

interface Props {
    name: string;
}

const Greeting: React.FC = ({ name }) => {
    return 

Hello, {name}

; }; export default Greeting;

Output: Renders a greeting message with the name passed in as a prop.

Description: This example shows how to use TypeScript with React, defining props with type annotations to ensure type safety in components.

TypeScript with Vue



Output: Displays a message in the Vue component.

Description: This example shows how TypeScript can be used with Vue components, defining the data model with types for better type safety.