Notes

TypeScript is a powerful, statically typed superset of JavaScript designed to help developers write safer, more maintainable, and error-free code. It enhances JavaScript by adding syntax for types, focusing on type safety. This means many issues can be caught during development, before the code even runs.

Why TypeScript?

  • Type Safety: TypeScript’s core benefit is static typing, which means variable types are checked at compile-time.

    • This contrasts with dynamically typed languages like JavaScript, where types are determined at runtime and can change, potentially leading to unexpected errors.
    • TypeScript ensures that a variable declared as a number will only store number values, preventing errors like trying to add a number to a string and getting a concatenated string result (e.g., 2 + "2" becoming "22").
  • Maintainability and Robustness: By catching type-related errors early, TypeScript makes code more robust and easier to maintain, especially in large projects and collaborative environments.

  • Readability and Clarity: Explicit type annotations improve code readability, making it clearer to both developers and the TypeScript compiler what types of values variables and functions are expected to handle.

  • Industry Demand: TypeScript is a valuable skill in the job market due to its high industry demand.

  • Efficiency and Error Prevention: It reduces the need for extensive unit testing, saving development time, and provides instant feedback on errors before runtime, leading to faster and more reliable development.

  • Seamless Integration: TypeScript integrates seamlessly with existing JavaScript code, making adoption painless.

  • Framework Empowerment: It enhances popular frameworks like React, Vue, and Angular with advanced features such as interfaces.

How TypeScript Works: Transpilation

TypeScript code (.ts files) is not directly executed by browsers or Node.js. Instead, it is transpiled (or compiled) into plain JavaScript code (.js files) by the TypeScript compiler (TSC). This JavaScript output can then be run in any JavaScript environment.

graph LR
    A["TypeScript Code (.ts)"] --> B["TypeScript Compiler (TSC)"]
    B --> C["JavaScript Code (.js)"]
    C --> D["JavaScript Runtime (Browser, Node.js)"]

Flowchart: TypeScript Compilation Process

Setting Up Your TypeScript Environment

To work with TypeScript, you’ll need:

  1. Git: A version control system.

  2. Visual Studio Code (VS Code): A popular code editor with excellent TypeScript support.

  3. Node.js: Includes npm (Node Package Manager), which is used to install TypeScript.

    • Verify Node.js Installation: Open your terminal and run node -v. It should display the installed Node.js version.
  4. TypeScript: Install TypeScript globally using npm.

    • Install Command: npm install -g typescript

    • Verify TypeScript Installation: After installation, run tsc -v. It should display the installed TypeScript version.

Your First TypeScript Application

  1. Create a folder: For example, TypeScript on your desktop.

  2. Create a file: Inside the TypeScript folder, create index.ts and open it in VS Code.

  3. Write code:

    console.log(Math.floor());

    You’ll immediately notice VS Code highlighting Math.floor as an error, indicating “Expected 1 argument, but got 0”. This is TypeScript’s type checking in action, catching errors before you run the code.

  4. Fix the error: Add a number argument, e.g., 11.3.

    console.log(Math.floor(11.3));
  5. Compile TypeScript to JavaScript:

    • Open your terminal and navigate to your TypeScript folder.

    • Run tsc index.ts. This will generate an index.js file in the same directory.

  6. Run the JavaScript file:

    • In the terminal, run node index.js. You should see the output in the console.

Project Configuration with tsconfig.json

For larger projects, it’s beneficial to configure the TypeScript compiler using a tsconfig.json file.

  1. Structure your project: Create a src folder and move index.ts into it.

  2. Generate tsconfig.json: In your project’s root directory, run tsc --init. This command creates a tsconfig.json file with default compiler options.

  3. Customize tsconfig.json: Open the tsconfig.json file. Two important options are:

    • rootDir: Defines the root directory for your TypeScript source files. Set it to "./src".

    • outDir: Specifies the output directory for compiled JavaScript files. Set it to "./dist" (or best as in the source).

    • Save your changes.

  4. Compile with tsconfig.json: From your project root, run tsc. TypeScript will now use the settings defined in tsconfig.json to compile your code. This will generate index.js inside the dist (or best) folder.

  5. Run compiled code: node dist/index.js (or node best/index.js).

graph TD
    A[Project Root] --> B[src/index.ts]
    A --> C[tsconfig.json]
    C -- rootDir: ./src --> B
    C -- outDir: ./dist --> D[dist/index.js]
    B -- tsc command --> D

Flowchart: Project Structure with tsconfig.json

Basic Types in TypeScript

TypeScript extends JavaScript’s built-in types (numbers, strings, booleans, null, undefined, objects) with additional types for enhanced safety and expressiveness.

  • JavaScript Built-in Types:

    • number

    • string

    • boolean

    • null

    • undefined

    • object

  • TypeScript Introduced Types:

    • any: Represents any type of value. It disables type checking for that variable. Avoid using any whenever possible as it negates type safety, reduces code readability, and makes it less maintainable.

    • unknown: A safer alternative to any.

    • never: Represents values that never occur, often associated with functions that throw exceptions or enter infinite loops.

    • enum: Defines a set of named constant values.

    • tuple: Similar to arrays but with a fixed number of elements and specific types for each position.

Type Annotation

Type annotations explicitly specify the type of a variable, function, or other entity using a colon (:) followed by the type. This helps the TypeScript compiler enforce intended usage.

  • With Variables:

    let myNumber: number = 10; // Explicitly annotated as number
    let myString: string = "Hello"; // Explicitly annotated as string

    Even if you declare a variable and assign a value later, TypeScript understands the type based on the initial declaration.

  • Dynamic Type Determination (Type Inference): If you don’t use type annotations, TypeScript can often infer the type based on the assigned value.

    let greeting = "Hello, TypeScript!"; // TypeScript infers 'greeting' as string

    While convenient, it’s generally best practice to use type annotations for clarity and explicit type safety, as relying solely on inference can sometimes lead to unexpected issues.

Type Annotation with Objects

Type annotations with objects allow you to define the types of properties an object should have.

type Address = {
    street: string;
    city: string;
};
 
type Person = {
    name: string;
    age: number;
    jobTitle?: string; // Optional property
    address: Address; // Nested object with type annotation
};
 
let personExample2: Person = {
    name: "Alice",
    age: 30,
    address: {
        street: "123 Main St",
        city: "Anytown"
    }
};
// 'jobTitle' is optional and can be omitted

Type Annotation with Functions

Type annotations for functions explicitly define the data types for parameters and return values.

function calculateRectangleArea(length: number, width: number): number { // Parameters and return type annotated
    return length * width; //
}
 
let area: number = calculateRectangleArea(10, 5); //
console.log(area);
Optional and Default Parameters

Functions can have optional parameters (marked with ?) or parameters with default values (using =).

// Optional parameter
function greet(name: string, greeting?: string): void {
    if (greeting) {
        console.log(`${greeting}, ${name}!`);
    } else {
        console.log(`Hello, ${name}!`);
    }
}
 
greet("Alice"); // Hello, Alice!
greet("Bob", "Hi"); // Hi, Bob!
 
// Default parameter
function sayHello(name: string = "Guest"): void {
    console.log(`Hello, ${name}!`);
}
 
sayHello(); // Hello, Guest!
sayHello("Charlie"); // Hello, Charlie!
Rest Parameters

Rest parameters allow a function to accept an indefinite number of arguments as an array, using the spread operator (...).

function addAll(...nums: number[]): number { // nums will be an array of numbers
    let sum = 0;
    for (let i = 0; i < nums.length; i++) { //
        sum += nums[i];
    }
    return sum;
}
 
console.log(addAll(1, 2, 3)); // 6
console.log(addAll(10, 20, 30, 40)); // 100
Arrow Functions and Anonymous Functions
  • Arrow Functions: Provide a more compact syntax for defining functions.

    const add = (num1: number, num2: number): number => num1 + num2; //
  • Anonymous Functions: Functions without a name, often defined as an expression. Useful for small, one-off functions to avoid polluting the global scope.

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

void and never Types

  • void: Indicates the absence of a value. Used for functions that don’t return any value or return undefined (e.g., functions that perform actions or side effects).

    function logMessage(message: string): void {
        console.log(message);
    }
  • never: Represents values that never occur. Typically associated with functions that throw exceptions, enter infinite loops, or have unreachable code.

    function throwError(message: string): never { //
        throw new Error(message);
    }
     
    function infiniteLoop(): never { //
        while (true) {
            // ...
        }
    }

Union Types

Union types allow a variable to hold values of multiple specified types, denoted by a pipe (|) symbol.

let myVar: string | number; // Can be a string or a number
myVar = "hello"; // Valid
myVar = 123; // Valid
// myVar = true; // Error: Type 'boolean' is not assignable to type 'string | number'.

Literal Types

Literal types allow you to specify exact values that are allowed for a variable or parameter, rather than just a general type.

type Direction = "left" | "right" | "up" | "down"; //
 
let playerDirection: Direction = "left"; // Valid
// playerDirection = "forward"; // Error: Type '"forward"' is not assignable to type 'Direction'.
 
function setColor(color: "red" | "green" | "blue"): void { //
    console.log(`Selected color: ${color}`);
}
 
setColor("red"); // Valid
// setColor("yellow"); // Error

Literal types enhance type safety by restricting input to a predefined set of values, catching errors at compile-time.

Nullable Types

Nullable types allow a variable or parameter to hold either a specific data type (like string or number) or the value null. This is useful for scenarios where a value might be absent or undefined. You create them by appending | null to an existing type.

let username: string | null = "John Doe"; // Can be string or null
username = null; // Valid
 
let age: number | null = 25; // Can be number or null
age = null; // Valid
 
function displayUsername(name: string | null): void { //
    if (name === null) {
        console.log("Welcome, Guest!"); //
    } else {
        console.log(`Welcome, ${name}!`);
    }
}
 
displayUsername("Alice");
displayUsername(null);

Nullable types improve code safety and clarity by explicitly indicating the possibility of missing data.

Type Aliases

Type aliases allow you to create custom names for types, improving readability, maintainability, and reusability for complex type definitions.

type MyString = string; // Alias for string type
type StringOrNumber = string | number; // Alias for a union type
 
let myName: MyString = "Bob"; // Using type alias
let myValue: StringOrNumber = 42; // Using type alias
 
// Type alias with objects
type Employee = {
    name: string; // Required string
    age: number; // Required number
    email?: string; // Optional string
};
 
let alice: Employee = {
    name: "Alice",
    age: 30,
    email: "alice@example.com"
}; //
 
let bob: Employee = {
    name: "Bob",
    age: 25
}; // Omits optional email

Type aliases provide a way to define complex object structures and reuse them throughout your code.

Intersection Types

Intersection types combine multiple types into one, creating a new type that possesses all properties and functionalities of the individual types being intersected. They are denoted by the & operator.

type FirstType = {
    prop1: string;
};
 
type SecondType = {
    prop2: number;
};
 
type CombinedType = FirstType & SecondType; // Combines properties of both
 
let myCombinedObject: CombinedType = {
    prop1: "hello",
    prop2: 123
};

Type Annotation with Arrays

Arrays in TypeScript can be type-annotated to specify the expected data type of their elements.

let fruits: string[] = ["apple", "banana", "orange"]; // Array of strings
 
for (let fruit of fruits) {
    console.log(fruit.toUpperCase()); //
}
 
let numbers: number[] = [1, 2, 3]; // Array of numbers
 
let mixedArray: (string | number)[] = ["hello", 1, "world", 2]; // Array of strings or numbers

Tuples

A tuple is a data type similar to an array but with a fixed number of elements, where you can specify the type of each element at a specific position.

let article: [number, string, boolean] = [1, "TypeScript Tutorial", true]; //
 
// Reassigning a new tuple value that matches the type structure
article = [2, "Advanced TypeScript", false];
 
// Tuples have fixed size; cannot add additional elements
// article.push("new element"); // Error
 
// Destructuring to extract elements
const [id, title, published] = article;
console.log(`ID: ${id}, Title: ${title}, Published: ${published}`);

Enums

Enums (enumerations) define a set of named constant values, providing a readable and expressive way to work with discrete options or categories. By default, enum members are assigned numeric values starting from 0.

enum Days {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
} //
 
let today: Days = Days.Wednesday; //
console.log(today); // Output: 3 (default numeric value)
console.log(Days[today]); // Output: Wednesday (retrieving name from value)
 
enum StatusCodes {
    OK = 200,
    BadRequest = 400,
    NotFound = 404
}
 
let currentStatus: StatusCodes = StatusCodes.OK;
console.log(currentStatus); // Output: 200

Enums improve code readability by providing human-readable names for specific values and are commonly used for categories like days of the week or status codes.

Interfaces

An interface in TypeScript defines a contract or blueprint for the shape and structure of an object. It specifies properties, methods, and their types that objects implementing the interface should have.

  • Shape Definition: Interfaces specify object structure, including property names, types, and optional/required status.

  • Contract: Objects or classes adhering to an interface must implement the defined structure and methods.

  • Type Checking: TypeScript checks if an object meets an interface’s requirements, catching type errors early.

  • Code Clarity: Interfaces document expected object properties and enhance code readability.

interface PersonExample1 {
    name: string;
    age: number;
} //
 
let alice: PersonExample1 = {
    name: "Alice",
    age: 30
}; //
Interface Methods and Parameters

Interfaces can also define the signatures of functions or methods that an object adhering to the interface should have, including parameters and return types.

interface PersonExample2 {
    name: string; // Property type string
    age: number;  // Property type number
    greet(message: string): void; // Method signature: takes string, returns void
}
 
let aliceWithMethod: PersonExample2 = {
    name: "Alice",
    age: 30,
    greet: function(message: string): void { //
        console.log(`Hello ${this.name}, ${message}`); //
    }
};
 
aliceWithMethod.greet("nice to meet you!"); // Method invocation
Interface Reopening (Declaration Merging)

Interface reopening allows you to define multiple interfaces with the same name, and TypeScript will merge them into a single interface. This promotes modularity and code organization, allowing different parts of a project to contribute to an interface’s definition.

interface Settings {
    theme: string;
    font: string;
} // Initial interface
 
// Reopen the interface to add a property specific to an 'articles' page
interface Settings {
    sidebar?: boolean; // Optional property
}
 
// Reopen the interface again for a 'contact' page
interface Settings {
    externalLinks?: boolean;
}
 
let userSettings: Settings = {
    theme: "dark",
    font: "Arial",
    sidebar: true,
    externalLinks: false
}; // userSettings compiles with the merged interface
Built-in Interfaces

TypeScript provides built-in interfaces for common JavaScript constructs and browser APIs, such as HTML elements. For example, HTMLElement, HTMLDivElement, and HTMLImageElement. These interfaces define properties and methods available for working with DOM elements, ensuring type safety when manipulating them.

Example: HTMLImageElement interface properties and methods:

  • Properties: alt (string), height (number), src (string), width (number).

  • Methods/Properties (status): complete (boolean), decode (Promise), naturalHeight (number), naturalWidth (number).

Interface vs. Type Alias
FeatureInterfaceType Alias
Declarationinterface keywordtype keyword
Extending/ImplementingCan be extended by other interfaces and implemented by classesCannot be directly extended or implemented, but similar results can be achieved with intersection types
Declaration MergingSupports declaration merging (multiple interfaces with same name are merged)Does not support declaration merging (multiple type aliases with same name cause an error)
Use CasesDefining the shape of objects, especially for public APIs, and defining contracts for classesCreating custom types that can be combined using union or intersection types, defining complex types, and giving descriptive names to combinations of types
Readability/StyleOften preferred for object shapes due to clear intent and better editor support (autocompletion, docs)Useful for defining union, intersection, and other complex types

In most cases, the choice depends on your specific use case and coding style. Both are valuable tools for defining custom types and enhancing code quality.

Classes in TypeScript

A class serves as a blueprint for creating objects with shared properties and methods, aiding in the organization and structure of your code. Class type annotations specify the types of properties, methods, and constructor parameters, enabling TypeScript to perform type checking and ensure instances adhere to the defined structure.

class Product {
    id: number; // Property type annotation
    name: string; // Property type annotation
    price: number; // Property type annotation
 
    constructor(id: number, name: string, price: number) { // Constructor parameter type annotation
        this.id = id;
        this.name = name;
        this.price = price;
    }
 
    getProductInfo(): string { // Method return type annotation
        return `Product ID: ${this.id}, Name: ${this.name}, Price: $${this.price}`;
    }
}
 
let productOne = new Product(1, "Laptop", 1200); // Creating an instance
console.log(productOne.getProductInfo()); // Calling method

Class Access Modifiers

Access modifiers control the visibility and accessibility of class members (properties and methods) from outside the class.

  • public: Members are accessible from anywhere, both within and outside the class. This is the default modifier.

    class MyClass {
        public myPublicProperty: string = "I am public";
    }
    let instance = new MyClass();
    console.log(instance.myPublicProperty); // Accessible
  • private: Members are only accessible from within the class where they are defined. They cannot be accessed from outside.

    class MyClass {
        private myPrivateProperty: string = "I am private";
        public showPrivate() {
            console.log(this.myPrivateProperty); // Accessible inside
        }
    }
    let instance = new MyClass();
    // console.log(instance.myPrivateProperty); // Error
    instance.showPrivate();
  • protected: Members are accessible from within the class they are defined in, and from subclasses (derived classes). They cannot be accessed from outside the class or unrelated classes.

    class Parent {
        protected protectedProperty: string = "I am protected";
    }
     
    class Child extends Parent {
        public showProtected() {
            console.log(this.protectedProperty); // Accessible in child class
        }
    }
     
    let parentInstance = new Parent();
    // console.log(parentInstance.protectedProperty); // Error (cannot access from outside)
    let childInstance = new Child();
    childInstance.showProtected();

These modifiers help encapsulate internal details and control how class members are manipulated, crucial for code maintainability, integrity, and security.

Class Accessors (Getters and Setters)

Accessors (getters and setters) control access to class properties, allowing you to get and set values while providing additional logic or validation if needed. They are defined using the get and set keywords.

class ProductAccessors {
    private _price: number; // Private property to store the actual price
 
    constructor(price: number) {
        this._price = price;
    }
 
    get price(): number { // Getter method to retrieve price
        return this._price;
    }
 
    set price(newPrice: number) { // Setter method to set price with validation
        if (newPrice < 0) {
            console.log("Price cannot be negative."); //
            return;
        }
        this._price = newPrice;
    }
 
    getProductInfo(): string {
        return `Product Price: $${this.price}`; // Uses the getter
    }
}
 
let productWithAccessors = new ProductAccessors(100);
console.log(productWithAccessors.price); // Accessing getter
productWithAccessors.price = 150; // Using setter
productWithAccessors.price = -20; // Setter logic prevents negative price

Class Static Members

Static members (properties or methods) belong to the class itself, not to instances of the class. They are accessed directly on the class name without creating an instance, using the static keyword.

class ProductStaticMembers {
    private static nextId: number = 1; // Private static property to manage unique IDs
 
    id: number;
    name: string;
 
    constructor(name: string) {
        this.id = ProductStaticMembers.generateNextId(); // Using static method
        this.name = name;
    }
 
    private static generateNextId(): number { // Static method to generate next ID
        return ProductStaticMembers.nextId++;
    }
 
    getProductInfo(): string {
        return `Product ID: ${this.id}, Name: ${this.name}`;
    }
}
 
let product1 = new ProductStaticMembers("Keyboard");
let product2 = new ProductStaticMembers("Mouse");
 
console.log(product1.getProductInfo());
console.log(product2.getProductInfo());

Class Implements Interface

A class can implement an interface, ensuring that it provides all the properties and methods required by that interface. This helps enforce a consistent structure for objects created from that class.

interface ProductImplementInterface { // Interface defining product structure
    id: number;
    name: string;
    getProductInfo(): string;
}
 
class ProductClass implements ProductImplementInterface { // Class implementing the interface
    id: number;
    name: string;
 
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
 
    getProductInfo(): string { // Must implement this method as per interface
        return `Product ID: ${this.id}, Name: ${this.name}`;
    }
}
 
let productOne: ProductImplementInterface = new ProductClass(1, "Monitor"); // Assigning to interface type
let productTwo: ProductImplementInterface = new ProductClass(2, "Webcam");
 
console.log(productOne.getProductInfo());
console.log(productTwo.getProductInfo());

Abstract Classes and Members

Abstract classes serve as blueprints for other classes. They cannot be instantiated on their own but can be subclassed. Abstract classes can contain abstract methods, which are declared but not implemented in the abstrac1t class itself. Subclasses are required to provide implementations for these abstract methods.

abstract class AbstractItem { // Abstract class
    private static nextId: number = 1; // Private static property
    id: number;
    name: string;
 
    constructor(name: string) {
        this.id = AbstractItem.generateNextId();
        this.name = name;
    }
 
    private static generateNextId(): number { // Static method
        return AbstractItem.nextId++;
    }
 
    abstract getItemInfo(): string; // Abstract method, must be implemented by subclasses
}
 
class ConcreteItem extends AbstractItem { // Concrete class extending abstract class
    constructor(name: string) {
        super(name); // Call parent constructor
    }
 
    getItemInfo(): string { // Implementation of the abstract method
        return `Item ID: ${this.id}, Name: ${this.name}`;
    }
}
 
let itemOne = new ConcreteItem("Book"); //
let itemTwo = new ConcreteItem("Pen");
 
console.log(itemOne.getItemInfo()); //
console.log(itemTwo.getItemInfo());

Polymorphism and Method Overriding

  • Polymorphism: Allows objects of different classes to be treated as objects of a common superclass.

  • Method Overriding: Occurs when a subclass provides a specific implementation of a method that is already defined in its superclass, allowing the subclass to customize the behavior.

abstract class AbstractEntity { // Abstract base class
    private static nextId: number = 1;
    protected name: string; // Protected to allow access in derived classes
    id: number;
 
    constructor(name: string) {
        this.id = AbstractEntity.nextId++;
        this.name = name;
    }
 
    abstract getEntityInfo(): string; // Abstract method
}
 
class Entity extends AbstractEntity { // Concrete subclass
    constructor(name: string) {
        super(name); // Call parent constructor
    }
 
    getEntityInfo(): string { // Overridden method
        return `Entity ID: ${this.id}, Name: ${this.name}`;
    }
}
 
class AnotherEntity extends AbstractEntity { // Another concrete subclass
    constructor(name: string) {
        super(name); // Call parent constructor
    }
 
    getEntityInfo(): string { // Overridden method
        return `Another Entity ID: ${this.id}, Specific Name: ${this.name}`;
    }
}
 
let entity1: AbstractEntity = new Entity("User"); // Polymorphism: stored as AbstractEntity type
let entity2: AbstractEntity = new AnotherEntity("Product"); // Polymorphism: stored as AbstractEntity type
 
console.log(entity1.getEntityInfo()); // Calls overridden method in Entity class
console.log(entity2.getEntityInfo()); // Calls overridden method in AnotherEntity class

Key Difference: A class is for creating objects, while an interface is for defining the shape of an object.

Generics in TypeScript

Generics are a powerful feature that allows you to write reusable code by passing types as parameters to other types (classes, interfaces, or functions). This enables working flexibly with various types without resorting to the any type.

Advantages of Generics:

  • Code Reusability: Use the same code with different types without rewriting it.

  • Enhanced Type Safety: Helps detect potential errors at compile time rather than runtime.

  • Dealing with Multiple Types: Allows working with many types without specifying a particular one.

Generics can be used to create generic classes, functions, interfaces, and methods.

Generic Functions

function returnType<T>(value: T): T { // Generic function: takes type T, returns type T
    return value;
}
 
console.log(returnType<number>(123)); // Explicitly specify type parameter
console.log(returnType<string>("hello"));
console.log(returnType<boolean>(true));
console.log(returnType<number[]>([1, 2, 3]));

The function provides type safety, ensuring the return value has the same type as the input value.

Generics with Multiple Types

Generics can handle multiple types using union types or intersection types, creating flexible and versatile code.

// Generic arrow function
const returnTypeExample = <T>(value: T): T => value; //
console.log(returnTypeExample<number>(100));
console.log(returnTypeExample<string>("Test"));
 
// Generic function with multiple types
function multipleTypeFunction<T, S>(value1: T, value2: S): string { // Accepts two different types T and S
    return `Value 1: ${value1} (Type: ${typeof value1}), Value 2: ${value2} (Type: ${typeof value2})`;
}
 
console.log(multipleTypeFunction<string, number>("Name", 30));
console.log(multipleTypeFunction<boolean, string>(true, "Status"));

Generic Classes

Generic classes allow flexible and reusable class structures that work with a variety of data types, ensuring code flexibility and type safety.

class User<T = string> { // Generic class with default type parameter T = string
    value: T;
 
    constructor(initialValue: T) { // Constructor takes initial value of type T
        this.value = initialValue;
    }
 
    show(message: T): void { // Method works with type T
        console.log(`Message: ${message}, Value: ${this.value}`);
    }
}
 
let userOne = new User<string>("Alice"); // User instance with string type parameter
userOne.show("Hello");
 
let userTwo = new User<number>(123); // User instance with number type parameter
userTwo.show(456);
 
let userThree = new User("Default String User"); // Uses default string type for T
userThree.show("Hi there!");

Generics in Interfaces

Generics in interfaces allow creating reusable and type-safe data structures that work with different types, enhancing code flexibility and maintainability.

interface Book {
    title: string;
    author: string;
} //
 
interface Game {
    name: string;
    platform: string;
} //
 
interface Collection<T> { // Generic interface for a collection of type T
    data: T[];
    add(item: T): void;
}
 
class ItemCollection<T> implements Collection<T> { // Generic class implementing the generic interface
    data: T[] = [];
 
    add(item: T): void {
        this.data.push(item);
    }
}
 
let bookCollection: ItemCollection<Book> = new ItemCollection<Book>(); // Collection instance for Book type
bookCollection.add({ title: "The Hobbit", author: "J.R.R. Tolkien" });
 
let gameCollection: ItemCollection<Game> = new ItemCollection<Game>(); // Collection instance for Game type
gameCollection.add({ name: "Cyberpunk 2077", platform: "PC" });
 
console.log(bookCollection.data);
console.log(gameCollection.data);

Type Assertions

Type assertions explicitly inform the TypeScript compiler about the expected type of a value, even if the compiler cannot automatically determine it. It’s like “telling” TypeScript to trust you about the type.

let someValue: any = "this is a string"; // 'any' type
 
// Using 'as' syntax for type assertion
let strLength: number = (someValue as string).length; // Asserting 'someValue' as string
console.log(strLength); // Output: 16
 
// Using angle-bracket syntax (less common with JSX)
let anotherValue: any = "hello world";
let upperCaseString: string = (<string>anotherValue).toUpperCase();
console.log(upperCaseString); // Output: HELLO WORLD

Caution: Use type assertions judiciously. While they allow you to override TypeScript’s type inference, incorrect assertions can lead to runtime errors, as TypeScript won’t perform actual type checks at runtime based on the assertion.

Debugging TypeScript Applications in VS Code

Debugging is invaluable for troubleshooting and ensuring your code behaves as expected.

  1. Enable Source Maps: In your tsconfig.json file, set sourceMap to true within the compilerOptions section. A source map (.js.map file) helps map compiled JavaScript code back to your original TypeScript code for debugging.

  2. Recompile your code: After enabling source maps, recompile your TypeScript code (e.g., tsc). You’ll see .js.map files generated alongside your .js files.

  3. Set Breakpoints: Open your .ts file and click on the line number in the gutter to set a breakpoint. Breakpoints pause code execution when reached.

  4. Create launch.json:

    • Go to the Debug panel in VS Code (usually the “Run and Debug” icon on the left sidebar).

    • Click on “create a launch.json file” (or the gear icon) and select “Node.js” from the dropdown. This configures VS Code for debugging.

  5. Add preLaunchTask: In your launch.json, add a preLaunchTask setting:

    {
        "version": "0.2.0",
        "configurations": [
            {
                "type": "node",
                "request": "launch",
                "name": "Launch Program",
                "skipFiles": [
                    "<node_internals>/**"
                ],
                "program": "${workspaceFolder}/dist/index.js", // Adjust path if your output is different
                "preLaunchTask": "tsc: build - tsconfig.json", // This tells VS Code to compile before launching
                "outFiles": [
                    "${workspaceFolder}/dist/**/*.js" // Adjust path
                ]
            }
        ]
    }
  6. Start Debugging: Go back to your .ts file, open the Debug panel, and click “Launch Program” or press F5.

  7. Debug Tools: Your program will start and pause at the breakpoint. In the Debug panel, you have tools like:

    • Step Over (F10): Execute one line of code.

    • Step Into: Step into a function call.

    • Step Out: Step out of the current function.

    • Restart: Restart the debugging session.

    • Stop: Terminate debugging.

  8. Inspect Variables: On the left, under “Variables,” you can see detected variables and their values as you step through the code. You can also add variables to the “Watch” window.

  9. Practice: Add console.log statements for extra inspection during debugging and use F5 to start and F10 to step through.

graph TD
    A[tsconfig.json] -- (set 'sourceMap': true) --> B[TypeScript Compiler]
    B -- compile .ts to .js + .js.map --> C[.ts file with breakpoints]
    D[VS Code Debug Panel] -- Create launch.json --> E[launch.json]
    E -- (set 'preLaunchTask': 'tsc: build') --> B
    C -- F5 (Launch Program) --> D
    D -- Debugging Tools --> F[Inspect Variables, Step through code]

Flowchart: Debugging in VS Code

Next Steps to Master TypeScript

  • Practice: The most crucial step to mastering any language is consistent practice. Start writing your own TypeScript code immediately.

  • Implement in Projects: If you’re using JavaScript frameworks (React, Next.js, etc.), begin implementing TypeScript in your new projects to experience its benefits in code quality and early error detection firsthand.

  • Explore Open-Source Projects: Examine how experienced developers use TypeScript in open-source projects on platforms like GitHub or GitLab. This “reverse engineering” helps you understand real-world applications.

  • Consult Official Documentation: Dive into the official TypeScript documentation. It’s an invaluable resource for in-depth knowledge and examples.

  • Deep Dive into Configuration: Explore the tsconfig.json file further. Learn about its various options and experiment with custom configurations to optimize TypeScript for your specific projects.


References