Typescript Interfaces

TypeScript interfaces are powerful tools that define the structure (properties) and, optionally, the shape (method signatures) of objects. They act as contracts that classes, functions, or other objects must adhere to, ensuring type safety and clarity in your code. This chapter delves into the world of TypeScript interfaces, covering everything from the basics to advanced concepts.

Basic Interfaces

Defining an Interface

You define an interface using the interface keyword followed by the interface name and curly braces {} to enclose its members:

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

				
			

This interface defines an object with two properties: name of type string and age of type number.

Implementing an Interface

Interfaces themselves don’t create objects. They act as blueprints. Classes, functions, or objects can implement the interface, meaning they must provide the required properties and adhere to the optional method signatures (if any) defined in the interface.

				
					class Employee implements Person {
  name: string;
  age: number;
  jobTitle: string; // Additional property not defined in the interface

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

const employee1: Person = new Employee("Alice", 30, "Software Engineer"); // Implement the Person interface

console.log(employee1.name); // Output: "Alice"
// console.log(employee1.jobTitle); // Error: jobTitle is not part of the Person interface

				
			

Optional Properties

Interfaces can have optional properties by adding a question mark ? after the property name:

				
					interface User {
  name: string;
  age?: number; // Optional property
}

const user1: User = { name: "Bob" }; // Valid: age is optional
const user2: User = { name: "Charlie", age: 25 }; // Valid: both properties provided

				
			

Interface Function Types

Defining Function Shapes

Interfaces can define the expected signature of functions, including parameter types and return type:

				
					interface GreetFunction {
  (name: string): string; // Function signature with parameter and return type
}

const greet: GreetFunction = (name: string) => {
  return "Hello, " + name + "!";
};

const message = greet("Alice");
console.log(message); // Output: "Hello, Alice!"

				
			

Functions can implement the defined function interface, ensuring they have the correct parameters and return type:

				
					interface Product {
  name: string;
  price: number;
  getInfo(discount?: number): string; // Function signature with optional parameter
}

class Shirt implements Product {
  name: string;
  price: number;

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

  getInfo(discount?: number): string {
    const priceWithDiscount = discount ? this.price * (1 - discount) : this.price;
    return `Shirt: ${this.name} - Price: $${priceWithDiscount.toFixed(2)}`;
  }
}

const shirt1 = new Shirt("T-Shirt", 20);
console.log(shirt1.getInfo());  // Output: "Shirt: T-Shirt - Price: $20.00"
console.log(shirt1.getInfo(0.1)); // Output: "Shirt: T-Shirt - Price: $18.00"

				
			

Extending Interfaces

Inheritance for Interfaces

Interfaces can inherit from other interfaces to create a hierarchy. The child interface inherits all the properties and methods of the parent interface and can add its own:

				
					interface Named {
  name: string;
}

interface Person extends Named {
  age: number;
}

const employee2: Person = { name: "Bob", age: 35 }; // Valid: implements both Named and Person

				
			

Index Signatures

Flexible Property Access

Index signatures allow you to define the type of properties that an object can have, even if their names are not known beforehand. This is useful for objects with dynamic properties or those coming from external sources (e.g., APIs).

				
					interface User {
  name: string;
  [key: string]: string | number; // Index signature with type constraints
}

const user: User = {
  name: "Alice",
  age: 30,
  city: "New York"
};

console.log(user.name);       // Output: "Alice"
console.log(user.age);        // Output: 30
console.log(user.city);       // Output: "New York"

// No errors accessing properties dynamically (assuming they match the type constraints)
user.country = "USA";
console.log(user.country);    // Output: "USA"

				
			

Interface with Class Types

Restricting Object Types

Interfaces can be used to restrict the type of objects that can be assigned to variables or function parameters:

				
					class Dog {
  name: string;
  breed: string;
}

class Cat {
  name: string;
  lives: number;
}

function printPetInfo(pet: Dog | Cat) { // Interface can restrict to Dog or Cat type
  console.log("Name:", pet.name);
  // Type specific properties can be accessed based on the actual object type at runtime
  if ("breed" in pet) {
    console.log("Breed:", pet.breed); // Only accessible for Dog objects
  } else if ("lives" in pet) {
    console.log("Lives:", pet.lives); // Only accessible for Cat objects
  }
}

const dog1 = new Dog();
dog1.name = "Max";
dog1.breed = "Labrador";

const cat1 = new Cat();
cat1.name = "Whiskers";
cat1.lives = 9;

printPetInfo(dog1);
// Output:
// Name: Max
// Breed: Labrador

printPetInfo(cat1);
// Output:
// Name: Whiskers
// Lives: 9

				
			

Readonly Interfaces

Enforcing Immutability

Readonly interfaces define objects where properties are set during initialization and cannot be modified afterward. This promotes immutability and prevents accidental data changes.

				
					interface ProductDetails {
  readonly name: string;
  readonly price: number;
}

const product: ProductDetails = { name: "Shirt", price: 20 };

// product.name = "T-Shirt"; // Error: cannot reassign readonly property
console.log(product.name); // Output: "Shirt"

				
			

Interface with Call Signatures

Function-like Interfaces

Interfaces can define call signatures to represent function-like objects. This allows you to work with objects that have a specific method structure:

				
					interface MathOperation {
  (x: number, y: number): number;
}

const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;

const result1 = add(5, 3);
console.log(result1); // Output: 8

const result2 = subtract(10, 4);
console.log(result2); // Output: 6

				
			

Intersection Types

Combining Interface Types

Intersection types combine multiple interfaces into a single type that requires the object to fulfill the requirements of all the combined interfaces.

				
					interface Named {
  name: string;
}

interface Dated {
  creationDate: Date;
}

interface User extends Named, Dated { // Intersection type
  email: string;
}

const user: User = {
  name: "Bob",
  creationDate: new Date(),
  email: "bob@example.com"
};

				
			

Generics in Interfaces

Flexible Interface Structure

Generics allow you to define interfaces that can work with different data types. This promotes code reusability and type safety.

				
					interface ArrayUtil<T> {
  items: T[];
  getFirst(): T;
  getLast(): T;
}

const numbers: ArrayUtil<number> = { items: [1, 2, 3] };
console.log(numbers.getFirst()); // Output: 1
console.log(numbers.getLast());  // Output: 3

const strings: ArrayUtil<string> = { items: ["Hello", "World"] };
console.log(strings.getFirst()); // Output: "Hello"
console.log(strings.getLast());  // Output: "World"

				
			

Mapped Types

Creating New Interfaces from Existing Ones

Mapped types allow you to create new interfaces by transforming the properties of an existing interface. This can be useful for creating variations or subsets of existing interfaces.

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

// ReadonlyUser has all properties of User marked as readonly
type ReadonlyUser = Readonly<User>;

const user1: User = { name: "Alice", age: 30 };
const readonlyUser: ReadonlyUser = { name: "Bob", age: 35 };

user1.name = "Charlie"; // Valid: name is mutable in User
// readonlyUser.name = "David"; // Error: name is readonly in ReadonlyUser

// PartialUser allows optional properties for all properties in User
type PartialUser = Partial<User>;

const partialUser: PartialUser = { name: "Eve" }; // Only name provided, valid

// Pick type allows selecting specific properties from User
type UserSummary = Pick<User, "name">;

const userSummary: UserSummary = { name: "Frank" }; // Only name property included

				
			

Conditional Types

Type Logic Based on Conditions

Conditional types allow you to define different types based on conditions within the interface definition. This enables type checks based on property values or other conditions.

				
					interface UserRole {
  name: string;
  role: "admin" | "editor" | "user";
  permissions: {
    [key: string]: boolean; // Index signature with conditional type
    canEdit?: boolean; // Optional property for non-admin roles
    canDelete?: boolean; // Optional property for admin role only
  };
}

const adminUser: UserRole = {
  name: "Alice",
  role: "admin",
  permissions: {
    canEdit: true,
    canDelete: true
  }
};

const editorUser: UserRole = {
  name: "Bob",
  role: "editor",
  permissions: {
    canEdit: true
  }
};

// Error: canDelete is not allowed for non-admin roles
// const nonAdminUser: UserRole = {
//   name: "Charlie",
//   role: "user",
//   permissions: {
//     canDelete: false
//   }
// };

				
			

Considerations

  • Interface Merging: Interfaces can be merged together to create a new interface that combines their properties.
  • Namespace Pattern: Namespaces can be used to organize related interfaces within larger projects.

Best Practices for TypeScript Interfaces

  • Use clear and descriptive interface names that reflect their purpose.
  • Employ type annotations for properties and methods to improve type safety.
  • Leverage optional properties for flexibility when not all properties are mandatory.
  • Consider readonly interfaces to enforce immutability where appropriate.
  • Explore generics to create interfaces that work with various data types.
  • Utilize mapped types and conditional types for advanced type manipulation.
  • Test your code thoroughly to ensure type safety and expected behavior.

TypeScript interfaces provide a powerful and versatile tool for defining the structure and behavior of objects in your code. By understanding the concepts explained in this chapter, you can write well-typed, maintainable, and flexible TypeScript applications. Remember to choose the appropriate interface features based on your specific needs and maintain a balance between complexity and readability. Happy coding !❤️

Table of Contents