TypeScript

TypeScript is a strongly typed programming language that builds on JavaScript. It adds optional static typing, classes, and modules to JavaScript, making it easier to develop large-scale applications. TypeScript compiles to clean, simple JavaScript code which runs on any browser, in Node.js, or in any JavaScript engine that supports ECMAScript 3 or newer.

2. Disadvantages of TypeScript

While TypeScript offers many benefits, it also has some drawbacks:

  • Additional Compilation Step: TypeScript needs to be compiled to JavaScript, which adds an extra step in the development process.
  • Learning Curve: Developers familiar with JavaScript need to learn new concepts like static typing.
  • Overly Complex Type System: It's possible to create very complex type definitions that can be hard to maintain.
  • Tooling Dependence: To get the most out of TypeScript, you need good tooling support, which may not be available in all environments.

3. Statically Typed Language

TypeScript is a statically typed language, which means the type of a variable is known at compile time. This is in contrast to JavaScript, which is dynamically typed. Static typing can help catch errors early in the development process and provide better tooling support.

4. Compiling a TypeScript Project

To compile a TypeScript file, you use the TypeScript compiler (tsc):

tsc index.ts

This will generate a JavaScript file (index.js) from your TypeScript file.

5. Setting Types

In TypeScript, you can explicitly set types:

1
2
3
let age: number = 20;
let name: string = "John";
let isStudent: boolean = true;

6. Types in TypeScript

Implicit vs Explicit Types

  • Implicit: TypeScript can infer types based on the assigned value.
    let name = "John"; // TypeScript infers type as string
    
  • Explicit: You can manually specify the type.
    let name: string = "John";
    

Any Type

The any type is a way to opt-out of type checking:

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

Unknown Type

The unknown type is similar to any, but safer because it's not legal to do anything with an unknown value:

1
2
3
4
5
6
7
let value: unknown = 10;
value = true;
value = "hello";

if (typeof value === "string") {
    console.log(value.toUpperCase()); // OK
}

Never Type

The never type represents the type of values that never occur:

1
2
3
function error(message: string): never {
    throw new Error(message);
}

Enum

Enums allow us to define a set of named constants:

1
2
3
4
5
6
enum Color {
    Red,
    Green,
    Blue,
}
let c: Color = Color.Green;

Tuple

Tuples allow you to express an array with a fixed number of elements whose types are known:

1
2
3
let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error

7. Objects

Object Types

You can define the shape of an object using object types:

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

Methods

Objects can have methods:

1
2
3
let calculator: { add: (x: number, y: number) => number } = {
    add: (x, y) => x + y,
};

Specific Values

You can specify that an object property must have a specific value:

1
2
3
4
let config: { readonly apiKey: string; debug: boolean } = {
    apiKey: "abc123",
    debug: true,
};

Return Type

You can specify the return type of a function:

1
2
3
function greet(): string {
    return "Hello, World!";
}

8. Type Aliases

Type aliases create a new name for a type:

1
2
3
4
5
6
type Point = {
    x: number;
    y: number;
};

let point: Point = { x: 10, y: 20 };

9. Union Types

Union types allow a value to be one of several types:

1
2
3
4
let result: number | string;
result = 10; // OK
result = "success"; // OK
result = true; // Error

10. Type Intersection

Intersection types combine multiple types into one:

type Employee = {
    name: string;
    startDate: Date;
};

type Manager = {
    name: string;
    department: string;
};

type TeamLead = Employee & Manager;

let teamLead: TeamLead = {
    name: "John",
    startDate: new Date(),
    department: "IT"
};

11. Literal Types

Literal types allow you to specify exact values:

1
2
3
let direction: "north" | "south" | "east" | "west";
direction = "north"; // OK
direction = "northeast"; // Error

12. Nullable Types

You can explicitly allow null or undefined:

1
2
3
4
5
let name: string | null = "John";
name = null; // OK

let age: number | undefined;
console.log(age); // undefined

13. Optional Properties, Elements, and Calls

You can mark properties, elements, or function parameters as optional using ?:

1
2
3
4
5
6
7
8
9
type User = {
    name: string;
    age?: number; // Optional property
};

function greet(name: string, greeting?: string) {
    // Optional parameter
    console.log(greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`);
}

14. Interfaces

Interfaces define the structure that objects must adhere to:

1
2
3
4
5
6
interface Person {
    name: string;
    age: number;
}

let user: Person = { name: "John", age: 30 };

Reopening Interfaces

You can add new properties to an existing interface:

1
2
3
4
5
interface Person {
    email: string;
}

let user: Person = { name: "John", age: 30, email: "[email protected]" };

Interface Inheritance

Interfaces can extend other interfaces:

1
2
3
interface Employee extends Person {
    employeeId: number;
}

15. Classes

Classes in TypeScript are similar to those in other object-oriented languages:

1
2
3
4
5
6
7
8
9
class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    move(distance: number = 0) {
        console.log(`${this.name} moved ${distance}m.`);
    }
}

Modifiers

TypeScript supports access modifiers:

  • public (default)
  • private
  • protected
1
2
3
4
5
class Person {
    private name: string;
    public constructor(name: string) { this.name = name; }
    public getName(): string { return this.name; }
}

Getters and Setters

You can use getters and setters to intercept access to a member of an object:

class Employee {
    private _fullName: string = "";

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        this._fullName = newName;
    }
}

Abstract Classes

Abstract classes are base classes from which other classes may be derived:

1
2
3
4
5
6
abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

Method Overriding

Derived classes can override methods from their base class:

1
2
3
4
5
class Dog extends Animal {
    makeSound(): void {
        console.log("Woof! Woof!");
    }
}

Difference Between Class and Abstract Class

  • Regular classes can be instantiated, while abstract classes cannot.
  • Abstract classes may contain abstract methods (without implementation) that must be implemented in derived classes.

16. Generics

Generics allow you to write reusable, type-safe code:

1
2
3
4
5
function identity<T>(arg: T): T {
    return arg;
}

let output = identity<string>("myString");

Generics can be used with functions, classes, and interfaces to create reusable components.


Generics in TypeScript

Generics provide a way to create reusable components that can work with a variety of types rather than a single one. They allow you to write functions, classes, and interfaces that can work with any data type while still providing compile-time type checking.

1. Generic Functions

Generic functions allow you to use type variables that are determined when the function is called.

Basic Example:

1
2
3
4
5
6
function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString");  // type of output1 is 'string'
let output2 = identity(42);  // type is inferred to be 'number'

Multiple Type Parameters:

1
2
3
4
5
6
function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

let p1 = pair<string, number>("hello", 42);  // [string, number]
let p2 = pair(true, [1, 2, 3]);  // [boolean, number[]]

2. Generic Interfaces

You can create generic interfaces to describe object shapes with flexible types.

interface Box<T> {
    contents: T;
}

let box1: Box<string> = { contents: "hello world" };
let box2: Box<number> = { contents: 42 };

interface Pair<T, U> {
    first: T;
    second: U;
}

let pair: Pair<string, number> = { first: "hello", second: 42 };

3. Generic Classes

Generic classes allow you to have type parameters for the entire class.

class Queue<T> {
    private data: T[] = [];

    push(item: T) {
        this.data.push(item);
    }

    pop(): T | undefined {
        return this.data.shift();
    }
}

let numberQueue = new Queue<number>();
numberQueue.push(10);
numberQueue.push(20);
console.log(numberQueue.pop());  // 10

let stringQueue = new Queue<string>();
stringQueue.push("hello");
stringQueue.push("world");
console.log(stringQueue.pop());  // "hello"

4. Generic Constraints

You can constrain the types that can be used with a generic using the extends keyword.

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property
    return arg;
}

loggingIdentity([1, 2, 3]);  // OK
loggingIdentity({length: 10, value: 3});  // OK
// loggingIdentity(3);  // Error, number doesn't have a .length property

5. Using Type Parameters in Generic Constraints

You can declare a type parameter that is constrained by another type parameter.

1
2
3
4
5
6
7
8
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");  // OK
getProperty(x, "m");  // Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

6. Generic Parameter Defaults

TypeScript allows you to specify default types for type parameters.

interface ApiResponse<T = any> {
    data: T;
    status: number;
}

function fetchApi<T = string>(url: string): Promise<ApiResponse<T>> {
    // implementation
    return Promise.resolve({ data: {} as T, status: 200 });
}

// Usage:
fetchApi("/users").then(response => {
    console.log(response.data);  // type is string
});

fetchApi<User>("/users").then(response => {
    console.log(response.data);  // type is User
});

7. Generic Utility Types

TypeScript provides several utility types that use generics:

Partial<T>

Makes all properties in T optional:

interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
    title: "organize desk",
    description: "clear clutter"
};

const todo2 = updateTodo(todo1, {
    description: "throw out trash"
});

Record<K,T>

Constructs a type with a set of properties K of type T:

1
2
3
4
5
6
7
8
type CatInfo = {age: number, breed: string};
type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {
    miffy: { age: 10, breed: "Persian" },
    boris: { age: 5, breed: "Maine Coon" },
    mordred: { age: 16, breed: "British Shorthair" }
};

Pick<T,K>

Constructs a type by picking the set of properties K from T:

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
    title: "Clean room",
    completed: false
};

8. Conditional Types with Generics

Conditional types allow you to create more complex type relationships:

type TypeName<T> = 
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

Type vs Interface in TypeScript

While type and interface in TypeScript can often be used interchangeably, there are some key differences between them. Let's explore these differences:

1. Basic Syntax

Type:

1
2
3
4
type User = {
  name: string;
  age: number;
};

Interface:

1
2
3
4
interface User {
  name: string;
  age: number;
}

2. Extending/Inheritance

Type:

Types use intersection to extend:

1
2
3
4
5
6
7
type Animal = {
  name: string;
};

type Bear = Animal & { 
  honey: boolean;
};

Interface:

Interfaces use the extends keyword:

1
2
3
4
5
6
7
interface Animal {
  name: string;
}

interface Bear extends Animal {
  honey: boolean;
}

3. Declaration Merging

Type:

Types cannot be re-opened to add new properties.

1
2
3
4
5
6
7
type Window = {
  title: string;
};

type Window = {
  ts: TypeScriptAPI;
}; // Error: Duplicate identifier 'Window'.

Interface:

Interfaces can be extended after being defined (declaration merging):

1
2
3
4
5
6
7
interface Window {
  title: string;
}

interface Window {
  ts: TypeScriptAPI;
} // This is valid and the Window interface now has both properties

4. Computed Properties

Type:

Can have computed properties:

1
2
3
4
5
type Keys = 'firstname' | 'surname';

type DudeType = {
  [key in Keys]: string;
};

Interface:

Cannot have computed property names:

1
2
3
interface DudeInterface {
  [key in Keys]: string; // Error: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
}

5. Union Types

Type:

Can define union types:

type MyUnion = string | number;
type TextOrNumber = { text: string } | { number: number };

Interface:

Cannot define union types (but can use them):

interface MyUnion = string | number; // Error
interface TextOrNumber = { text: string } | { number: number }; // Error

6. Utility Types

Type:

Works well with utility types:

type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;

Interface:

Can be used with utility types, but less commonly:

interface PartialUser extends Partial<User> {}

7. Tuples

Type:

Can easily describe tuples:

type Pair = [string, number];

Interface:

Can describe tuples, but it's more verbose:

1
2
3
4
5
interface Pair extends Array<string | number> { 
  0: string;
  1: number;
  length: 2;
}

8. Implements

Both can be implemented by classes:

1
2
3
4
5
6
7
8
9
class MyClass implements User {
  name: string;
  age: number;

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

9. Performance

For very complex types, the TypeScript compiler might be slightly faster with interfaces, but this is rarely a concern in practice.

Conclusion

In general:

  • Use interface when you want to define a contract for an object shape, especially if you might want to extend it later.
  • Use type when you need to create complex types involving unions, intersections, or mapped types.

Both type and interface can be used in many situations, and the choice often comes down to personal or team preference. The TypeScript team generally recommends using interface until you need to use features from type.


Interface vs Class

Direct Answer

In TypeScript, both classes and interfaces serve as blueprints for objects, but they differ significantly in their capabilities and intended use cases.

  • Classes are a fundamental part of object-oriented programming in TypeScript. They can contain properties, methods, and constructors. Classes support inheritance, allowing one class to inherit properties and methods from another. They also allow for member visibility control (public, private, protected). Importantly, classes exist in the compiled JavaScript output, meaning they contribute to the runtime behavior of your application.
  • Interfaces, on the other hand, are purely a TypeScript construct used for type-checking during compile time. They define the shape of an object—what properties and methods an object should have—but do not provide any implementation. Since interfaces are removed during the compilation process, they do not appear in the resulting JavaScript code. This makes them ideal for defining data shapes without adding any runtime overhead.

Example Code

Using an Interface

interface Vehicle {
  wheels: number;
  speed: number;
  accelerate(speedIncrease: number): void;
}

// Implementing the interface in a class
class Car implements Vehicle {
  wheels: number = 4;
  speed: number = 0;

  accelerate(speedIncrease: number) {
    this.speed += speedIncrease;
  }
}

Using a Class

class Vehicle {
  protected wheels: number;
  protected speed: number;

  constructor(wheels: number) {
    this.wheels = wheels;
    this.speed = 0;
  }

  accelerate(speedIncrease: number) {
    this.speed += speedIncrease;
  }
}

// Extending the class
class Car extends Vehicle {
  constructor() {
    super(4); // Call the parent class constructor with 4 wheels
  }
}

Access Modifiers

Access modifiers in TypeScript are keywords that set the accessibility level of class members (properties and methods). There are three types of access modifiers:

  1. public
  2. private
  3. protected

By default, all members of a class in TypeScript are public if no modifier is specified. Let's explore each modifier in detail with examples.

Public

Members declared as public can be accessed anywhere without restrictions. This is the default access level if none is specified.

1
2
3
4
5
6
7
8
class Greeter {
  public greet() { // 'public' keyword is optional here
    console.log("Hello, world!");
  }
}

const greeter = new Greeter();
greeter.greet(); // Outputs: Hello, world!

Private

Members marked as private can only be accessed within the class they are declared. Attempting to access them outside the class will result in a compile-time error.

class BankAccount {
  private balance: number;

  constructor(balance: number) {
    this.balance = balance; // OK: Accessed within the class
  }

  deposit(amount: number) {
    this.balance += amount; // OK: Accessed within the class
  }

  getBalance(): number {
    return this.balance; // OK: Accessed within the class
  }
}

const account = new BankAccount(100);
console.log(account.balance); // Error: Property 'balance' is private and only accessible within class 'BankAccount'.
account.deposit(50); // OK: Method call allowed
console.log(account.getBalance()); // OK: Method call allowed, outputs: 150

Protected

Members marked as protected can be accessed within the class they are declared and by subclasses. Like private, attempting to access protected members outside these bounds will result in a compile-time error.

class Person {
  protected name: string;

  constructor(name: string) {
    this.name = name; // OK: Accessed within the class
  }

  getName(): string {
    return this.name; // OK: Accessed within the class
  }
}

class Employee extends Person {
  constructor(name: string) {
    super(name); // OK: Accessed within subclass
  }

  promote() {
    console.log(`${this.name} has been promoted!`); // OK: Accessed within subclass
  }
}

const employee = new Employee("John Doe");
employee.promote(); // OK: Method call allowed, outputs: John Doe has been promoted!
console.log(employee.getName()); // OK: Method call allowed, outputs: John Doe
console.log(employee.name); // Error: Property 'name' is protected and only accessible within class 'Person' and its subclasses.

Summary

  • Public: Members are accessible everywhere. This is the default.
  • Private: Members are only accessible within the class they are declared.
  • Protected: Members are accessible within the class they are declared and by subclasses.

Data Types

TypeScript provides a rich set of data types that enable developers to specify the type of data that variables, parameters, and object properties can store.

These types range from primitive types like number, string, and boolean to more complex types such as enum, array, tuple, and user-defined types like classes and interfaces.

Primitive Types

Number

TypeScript supports numeric values as numbers. This includes both integers and floating-point values.

let age: number = 25;
let price: number = 99.99;

String

Strings represent textual data. They can be enclosed in single quotes (' '), double quotes (" "), or backticks () for template literals.

1
2
3
let firstName: string = "John";
let lastName: string = 'Doe';
let fullName: string = `John ${lastName}`; // Template literal

Boolean

Booleans represent true/false values.

let isActive: boolean = true;
let isDisabled: boolean = false;

Special Types

Any

The any type is a powerful way to work with existing JavaScript, allowing you to opt-out of type-checking.

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

Unknown

Similar to any, but safer because anything is assignable to unknown, but unknown isn't assignable to anything but itself and any without a type assertion or a control flow based narrowing.

1
2
3
4
5
6
7
let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK

Complex Types

Enum

Enums allow us to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases.

1
2
3
4
5
6
7
enum Color {
  Red,
  Green,
  Blue,
}

let c: Color = Color.Green;

Array

Arrays can be declared in two ways: using the type of the elements followed by [], or using a generic array type Array<elemType>.

let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];

Tuple

Tuples allow you to express an array with a fixed number of elements whose types are known, but need not be the same.

let tuple: [string, number] = ["hello", 10]; // OK

User-defined Types

Interface

Interfaces define the shape of an object. They are purely a compile-time construct and get erased when transpiling to JavaScript.

1
2
3
4
5
6
7
8
9
interface Person {
  firstName: string;
  lastName: string;
}

const person: Person = {
  firstName: "John",
  lastName: "Doe"
};

Class

Classes support inheritance, short syntax for creating objects and dealing with inheritance, and will be familiar if you're coming from an object-oriented language.

class Animal {
  name: string;

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

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log('Woof! Woof!');
  }
}

const dog = new Dog('Rex');
dog.bark();
dog.move(10);

4 Pillars of Object Oriented Programming in TypeScript

The four pillars of Object-Oriented Programming (OOP) are Encapsulation, Inheritance, Polymorphism, and Abstraction.

Encapsulation

Encapsulation is the bundling of data, represented by properties, and methods that operate on that data into a single unit called a class. It also involves restricting access to some of the object's components, which is known as information hiding.

class BankAccount {
  private balance: number;

  constructor(balance: number) {
    this.balance = balance;
  }

  public deposit(amount: number): void {
    this.balance += amount;
  }

  public withdraw(amount: number): boolean {
    if (amount <= this.balance) {
      this.balance -= amount;
      return true;
    } else {
      return false;
    }
  }

  public getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount(100);
account.deposit(50); // OK
console.log(account.getBalance()); // OK, outputs: 150
// console.log(account.balance); // Error: Property 'balance' is private

Inheritance

Inheritance is a mechanism where one class acquires the properties and methods of another class. The class being inherited from is called the superclass or parent class, and the class doing the inheriting is called the subclass or child class.

class Vehicle {
  protected speed: number = 0;

  move(distanceInMeters: number = 0): void {
    console.log(`Vehicle moved ${distanceInMeters}m.`);
  }
}

class Car extends Vehicle {
  startEngine(): void {
    console.log("Car engine started.");
  }
}

const car = new Car();
car.startEngine(); // OK
car.move(100); // OK, outputs: Vehicle moved 100m.

Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. It enables one interface to represent a general class of actions, which means that the exact action is determined by the exact nature of the situation.

interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(private radius: number) {}

  area(): number {
    return Math.PI * Math.pow(this.radius, 2);
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  area(): number {
    return this.width * this.height;
  }
}

function calculateArea(shape: Shape): void {
  console.log(`Area: ${shape.area()}`);
}

const circle = new Circle(5);
const rectangle = new Rectangle(10, 20);

calculateArea(circle); // OK, outputs area of circle
calculateArea(rectangle); // OK, outputs area of rectangle

Abstraction

Abstraction means hiding complex details and showing only the essentials. In TypeScript, abstraction can be achieved using abstract classes and interfaces. An abstract class cannot be instantiated directly but can be extended by other classes.

abstract class Animal {
  abstract makeSound(): void;

  move(): void {
    console.log("Moving along!");
  }
}

class Dog extends Animal {
  makeSound(): void {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.makeSound(); // OK, outputs: Woof! Woof!
dog.move(); // OK, outputs: Moving along!
// const animal = new Animal(); // Error: Cannot create an instance of an abstract class.

Dependency Injection (DI) in TypeScript

Dependency Injection (DI) is a design pattern that promotes loose coupling between classes and their dependencies. Instead of hard-coding dependencies inside a class, DI allows dependencies to be injected into a class, usually through constructors or methods. This pattern enhances modularity, testability, and maintainability of the code. TypeScript, with its strong typing features, complements DI well, making it easier to manage dependencies explicitly.

Basic Dependency Injection Example

Consider a scenario where ServiceB depends on ServiceA. Without dependency injection, ServiceB might instantiate ServiceA directly within its own code, leading to tight coupling. With DI, ServiceA is passed to ServiceB, often through the constructor.

class ServiceA {
  doSomething(): void {
    console.log('ServiceA does something');
  }
}

class ServiceB {
  private serviceA: ServiceA;

  constructor(serviceA: ServiceA) {
    this.serviceA = serviceA;
  }

  execute(): void {
    this.serviceA.doSomething();
    console.log('ServiceB executes');
  }
}

const serviceA = new ServiceA();
const serviceB = new ServiceB(serviceA);
serviceB.execute(); // Outputs: ServiceA does something, then ServiceB executes

In this example, ServiceB receives an instance of ServiceA through its constructor, allowing it to call doSomething() without knowing how to create or manage ServiceA.

Using Dependency Injection Containers

While manual dependency injection (as shown above) is straightforward, managing dependencies can become complex in larger applications. Dependency Injection Containers (DI Containers), also known as Inversion of Control (IoC) containers, automate the process of creating objects and injecting their dependencies.

DI Containers:

  • Manage object creation and lifecycle.
  • Automatically resolve and inject dependencies.
  • Can be configured programmatically or via configuration files.

Several libraries offer DI Container functionality for TypeScript, such as InversifyJS, NestJS (which uses InversifyJS under the hood), and Microsoft's TypeScript DI container. These containers allow for more sophisticated dependency management, including handling circular dependencies, lazy initialization, and scoped lifetimes.


Decorators

Decorators in TypeScript are a powerful feature that allows you to add annotations and a meta-programming syntax for class declarations and members. Introduced as an experimental feature in TypeScript, decorators have evolved and are now supported natively in TypeScript 5.0, aligning with the ECMAScript Stage 3 proposal. This feature enables developers to modify or augment classes and class members at design time, offering a wide range of use cases from logging to dependency injection.

Enabling Decorators in TypeScript

Before TypeScript 5.0, decorators were considered an experimental feature and needed to be explicitly enabled in the tsconfig.json file by setting "experimentalDecorators": true. However, starting from TypeScript 5.0, decorators are enabled by default, and the experimental flag is no longer required.

Types of Decorators

There are four primary types of decorators in TypeScript:

  1. Class Decorators: Applied to the class declaration itself.
  2. Method Decorators: Applied to the constructor, method, accessor, property, or parameter of a class.
  3. Property Decorators: Applied to the property of a class.
  4. Accessor Decorators: Applied to the accessor of a class.

Examples

Class Decorator

A class decorator is applied to the class definition. It can be used to observe, modify, or replace a class definition.

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

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

Method Decorator

A method decorator is applied to the constructor, method, accessor, property, or parameter of a class.

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  let originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling '${propertyKey}' with args: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  @log
  greet() {
    return "Hello, " + this.greeting;
  }
}

Property Decorator

A property decorator is applied to the property of a class.

function enumerable(target: any, propertyKey: string) {
  Reflect.enumerable = true;
}

class Greeter {
  @enumerable
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
}

Accessor Decorator

An accessor decorator is applied to the accessor of a class.

function enumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.enumerable = true;
}

class Greeter {
  private _greeting: string;
  constructor(message: string) {
    this._greeting = message;
  }
  @enumerable
  get greeting() {
    return this._greeting;
  }
}

Type Assertion

Type assertion in TypeScript is a way to tell the compiler "trust me, I know what I'm doing." It allows you to override the inferred type of a variable, essentially letting you inform the compiler about the type of a variable. This can be particularly useful when you have a better understanding of the type of a variable than what TypeScript can infer on its own, especially when migrating code from JavaScript to TypeScript.

Syntax

Type assertion can be performed in two ways:

  1. Using Angle Brackets: <Type>variableName
  2. Using the as Keyword: variableName as Type

Example: Type Assertion with Numbers

1
2
3
let code: any = 123; 
let employeeCode = <number>code; 
console.log(typeof(employeeCode)); // Output: number

Or using the as keyword:

1
2
3
let code: any = 123; 
let employeeCode = code as number;
console.log(typeof(employeeCode)); // Output: number

In both examples, we assert that code is of type number, even though it was initially declared as any. This tells the TypeScript compiler to treat employeeCode as a number.

Example: Type Assertion with Objects

Suppose you have an object that hasn't been defined with any properties yet, but you know it will have certain properties later on.

1
2
3
4
5
6
7
8
interface Foo {
    bar: number;
    bas: string;
}

var foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';

Here, we assert that foo is of type Foo, allowing us to add properties bar and bas to it without TypeScript errors.

Double Assertion

Sometimes, you might encounter situations where TypeScript requires a double assertion. This is less common but can be necessary in certain scenarios, such as when you need to assert a variable to a more specific type that is not directly assignable from the current type.

1
2
3
function handler(event: Event) {
    let element = event as unknown as HTMLElement; // Okay!
}

In this example, event is of type Event, but we want to treat it as an HTMLElement. First, we assert it to unknown (which is compatible with all types), and then to HTMLElement.


Type Annotation

Type annotations in TypeScript are a way to explicitly specify the type of variables, function parameters, and object properties. TypeScript, being a statically typed superset of JavaScript, introduces type annotations to help developers catch errors early during development, improve code readability, and ensure that the code behaves as expected.

Declaring Variables with Type Annotations

You can declare variables with type annotations by specifying the type after the variable name, preceded by a colon. This informs the TypeScript compiler about the expected type of the variable, enforcing type safety.

1
2
3
let age: number = 32; // number variable
let name: string = "John"; // string variable
let isUpdated: boolean = true; // Boolean variable

Attempting to assign a value of a different type to these variables would result in a TypeScript compilation error, helping to prevent bugs related to incorrect data types.

Type Annotations in Functions

Function parameters and return types can also be annotated with types. This ensures that functions receive the correct types of arguments and specifies what type of value the function returns.

1
2
3
function display(id: number, name: string): void {
    console.log("Id = " + id + ", Name = " + name);
}

In this example, the display function expects two parameters: id of type number and name of type string. The function does not return a value, hence the return type is void.

Inline Type Annotations for Object Properties

Type annotations can be used to specify the types of properties within an object. This is particularly useful when defining the shape of an object, ensuring that all properties adhere to the expected types.

1
2
3
4
5
6
7
8
9
var employee: {
    id: number;
    name: string;
};

employee = {
  id: 100,
  name: "John"
};

In this case, the employee object is expected to have an id property of type number and a name property of type string. Assigning values of different types to these properties would result in a TypeScript error.

Why Use Type Annotations?

  • Type Safety: Type annotations help catch errors early by preventing assignments of incompatible types.
  • Readability and Maintenance: Explicitly stating the types of variables, parameters, and properties makes the code easier to read and maintain.
  • Tooling Support: Modern IDEs and editors can leverage type annotations to provide better autocompletion, refactoring tools, and inline documentation.

Type Guarding, Type Unknown, Type Any & Type Never

TypeScript introduces several unique types that help developers handle various programming scenarios more effectively. Among these, unknown, any, and never stand out for their utility in different contexts. Additionally, type guarding plays a crucial role in narrowing down types, especially when dealing with the unknown type. Let's delve into each of these concepts in detail.

Type Guarding

Type guards are expressions that perform runtime checks to guarantee the type within a certain scope. They are essential for working with the unknown type, as they allow you to safely access properties or methods that are specific to a particular type.

function isString(test: unknown): test is string {
  return typeof test === "string";
}

function printLength(input: unknown) {
  if (isString(input)) {
    console.log(input.length); // Safe to access .length here
  }
}

printLength("hello"); // Works
printLength(123); // Does not throw an error

In this example, isString is a type guard that narrows the type of test to string within the block where it's used. This is particularly useful when dealing with values of type unknown, as it allows for safe type checking and operation.

Type Unknown

The unknown type represents any value but is safer than any because you cannot perform arbitrary operations on values of type unknown without first asserting or narrowing to a more specific type.

1
2
3
4
5
6
7
8
9
function prettyPrint(x: unknown): string {
  if (typeof x === "string") {
    return `"${x}"`;
  }
  if (Array.isArray(x)) {
    return "[" + x.map(prettyPrint).join(", ") + "]";
  }
  return "etc.";
}

In prettyPrint, x is of type unknown, but by using type guards (typeof x === "string" and Array.isArray(x)), TypeScript can safely determine the type within each branch, allowing for accurate type-checking and operations.

Type Any

The any type is a powerful way to bypass TypeScript's static type checking. It essentially opts out of type checking for a variable, allowing it to hold any type of value. However, using any defeats the purpose of using TypeScript, as it removes the benefits of type safety.

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

While any can be useful in certain scenarios, it's generally recommended to avoid it unless absolutely necessary, as it undermines the type safety that TypeScript aims to provide.

Type Never

The never type represents values that never occur. For example, it's the return type of functions that never return a value (e.g., functions that throw an exception or enter an infinite loop).

1
2
3
function error(message: string): never {
  throw new Error(message);
}

In this example, error is a function that takes a message of type string and throws an error. Since throwing an error prevents normal execution flow, the return type of error is never.


Fetch API in TS and Declarations File

Fetching data in TypeScript, especially when dealing with external APIs, can be efficiently managed using the fetch API or libraries like node-fetch for Node.js environments. To ensure smooth integration and type safety, it's crucial to understand how to correctly declare and use these functionalities in TypeScript.

Using Fetch in TypeScript

The fetch API is widely used for making network requests in web browsers. When migrating to TypeScript, you might encounter issues with type definitions, especially since the native fetch API doesn't come with TypeScript typings by default.

To use fetch in TypeScript, you typically don't need to do anything special if you're running your code in a browser environment, as browsers already include the fetch API. However, for Node.js environments, you'd use node-fetch or similar libraries, which provide their own type definitions.

Using node-fetch in TypeScript

node-fetch is a light-weight module that brings the fetch API to Node.js, making it easy to make HTTP requests. To use node-fetch in TypeScript, you first need to install it and its type definitions.

  1. Install node-fetch and its type definitions:
    npm install node-fetch
    
  2. Import fetch in your TypeScript file:

    import fetch from 'node-fetch';
    

    Ensure your tsconfig.json is configured to accept modules:

    1
    2
    3
    4
    5
    6
    {
      "compilerOptions": {
        "module": "commonjs",
        ...
      }
    }
    
  3. Use fetch to make HTTP requests:
    1
    2
    3
    4
    5
    6
    const url = 'https://api.example.com/data';
    
    fetch(url)
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
    

Handling Type Definitions

When using node-fetch or similar libraries, TypeScript might complain about missing type definitions. This can be resolved by installing the corresponding type definitions package, such as @types/node-fetch for node-fetch.

However, as noted in Source 4, newer versions of node-fetch (v3 and above) bundle their own type definitions, so you might not need to install @types/node-fetch separately. Always check the library's documentation for the latest guidance on type definitions.

Declaration Files

Declaration files (.d.ts) in TypeScript provide type information about an API that isn't available in the TypeScript standard library. When you encounter a TypeScript error saying it couldn't find a declaration file for a module, it means TypeScript is expecting type definitions for that module but can't find them.

To fix this, you can manually download the type definitions from DefinitelyTyped (if available) or install them via npm (for libraries that include their own type definitions or have community-provided ones).


Type Alias & Intersection Type

Type Alias

Type alias in TypeScript allows you to create a new name for a type. This can be particularly useful for simplifying complex type definitions, reusing types across your project, or making your code more readable. You define a type alias using the type keyword followed by the alias name and the type it represents.

Example of Type Alias

1
2
3
4
5
type StringOrNumber = string | number;

function processInput(input: StringOrNumber) {
  // input can be either a string or a number
}

In this example, StringOrNumber is a type alias for the union type string | number. This makes the processInput function signature more readable and reusable.

Intersection Type

Intersection types in TypeScript allow you to combine multiple types into one. This is useful when you want an object to satisfy multiple interfaces or when you want to combine primitive types. The combined type will have all the properties of the constituent types.

Defining Intersection Types

You define an intersection type using the & operator between the types you want to combine.

interface Person {
  name: string;
}

interface Employee {
  employeeID: number;
}

type EmployeePerson = Person & Employee;

let employee: EmployeePerson = {
  name: "John Doe",
  employeeID: 12345,
};

In this example, EmployeePerson is an intersection type that combines Person and Employee. An object of type EmployeePerson must have both name and employeeID properties.

Important Notes

  • The order of types in an intersection does not affect the resulting type.
  • If there are overlapping properties in the types being combined, the resulting type will have those properties, potentially causing conflicts if not handled carefully.

Advanced Usage

Intersection types can be nested or combined with other advanced types, making them a powerful tool for modeling complex relationships between types.

type Address = {
  street: string;
  city: string;
};

type EmployeeWithAddress = Employee & { address?: Address };

let employeeWithAddress: EmployeeWithAddress = {
  ...employee,
  address: {
    street: "123 Main St",
    city: "Anytown",
  },
};

In this advanced example, EmployeeWithAddress is an intersection type that combines Employee with an optional address property. This demonstrates how intersection types can be used to extend existing types with additional properties.


tsconfig.json

The tsconfig.json file configures TypeScript compiler options for a project, specifying how TypeScript should behave when compiling the source code.

sample tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules"]
}

Compiler Options

  • target: Specifies the ECMAScript target version. Here, it's set to es5, meaning the output will be compatible with ES5 environments.
  • lib: Defines library files to be included in the compilation. Here, it includes DOM APIs (dom), iterable protocol support for DOM collections (dom.iterable), and the latest ECMAScript features (esnext).
  • allowJs: Allows JavaScript files to be compiled along with TypeScript files. This is useful when integrating existing JavaScript code with TypeScript.
  • skipLibCheck: Skips type checking of declaration files (*.d.ts). This can speed up compilation times.
  • esModuleInterop: Enables a more compatible CommonJS/AMD module import/export syntax. This is particularly useful when importing CommonJS modules in TypeScript projects.
  • allowSyntheticDefaultImports: Allows default imports from modules with no default export. This can make importing CommonJS modules more convenient.
  • strict: Enables all strict type-checking options. This makes TypeScript enforce stricter type checking, helping catch potential bugs at compile time.
  • forceConsistentCasingInFileNames: Ensures that references to the same file are consistent in casing. This helps avoid errors caused by inconsistent casing in imports.
  • noFallthroughCasesInSwitch: Prevents fall-through cases in switch statements without an explicit break statement. This improves code safety and readability.
  • module: Specifies the module code generation. Here, it's set to esnext, meaning the latest ECMAScript module syntax will be used.
  • moduleResolution: Determines how the compiler resolves module imports. node mode mimics Node.js module resolution strategy.
  • resolveJsonModule: Allows importing JSON modules. This enables importing JSON files directly in TypeScript.
  • isolatedModules: Requires each file to be self-contained and not rely on external declarations. This is useful for tree-shaking and optimizing output.
  • noEmit: Prevents the compiler from emitting output files. This can be used for type checking without generating JavaScript files.
  • jsx: Specifies how JSX syntax should be compiled. Here, it's set to react-jsx, indicating the use of React 17 or later syntax.

Include and Exclude

  • include: Specifies which files should be included in the compilation. Here, it includes TypeScript files (*.ts), React TypeScript files (*.tsx), and any TypeScript declaration file in the next-env.d.ts directory within the src folder.
  • exclude: Specifies directories to exclude from the compilation process. Here, it excludes the node_modules directory.
Edit
Pub: 09 Aug 2024 17:39 UTC
Edit: 09 Aug 2024 20:04 UTC
Views: 1960