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.
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
.
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
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
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"
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 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"
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 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"
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 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 allow you to define interfaces that can work with different data types. This promotes code reusability and type safety.
interface ArrayUtil {
items: T[];
getFirst(): T;
getLast(): T;
}
const numbers: ArrayUtil = { items: [1, 2, 3] };
console.log(numbers.getFirst()); // Output: 1
console.log(numbers.getLast()); // Output: 3
const strings: ArrayUtil = { items: ["Hello", "World"] };
console.log(strings.getFirst()); // Output: "Hello"
console.log(strings.getLast()); // Output: "World"
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;
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;
const partialUser: PartialUser = { name: "Eve" }; // Only name provided, valid
// Pick type allows selecting specific properties from User
type UserSummary = Pick;
const userSummary: UserSummary = { name: "Frank" }; // Only name property included
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
// }
// };
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 !❤️