DEV Community

navid barsalari
navid barsalari

Posted on

Slum Dunk in Typescript Interface

Summary: in this tutorial, you’ll learn about TypeScript interfaces and how to use them to enforce type-checking and we will explore interfaces in TypeScript.

Interfaces in TypeScript are much more powerful and have some strange capabilities, such as extending from each other and from classes! Today, we will review all of them together.

Image description
From Basics to Advanced Patterns and Best Practices Topics Covered:

  • Defining an Interface
  • Optional Properties
  • Implementing Interfaces in Classes
  • Multiple Interface Implementation
  • Implementing Interface for a Service Class
  • Combining Interfaces (Interface Composition)
  • Dynamic Implementation with Factory Pattern
  • Merging Interfaces
  • Generics in Interfaces
  • Interview Questions
  • Best Practices for TypeScript Interfaces

Introduction to TypeScript interfaces
TypeScript interfaces define the contracts within your code. They also provide explicit names for type checking.

What are TypeScript Interfaces?
At its core, an interface in TypeScript is a syntactical contract that defines the expected structure of an object. It provides a way to describe the shape of objects, including their properties and methods, without implementing any functionality. Interfaces solely focus on the structure and type-checking aspects, allowing for better code understanding and validation during development.

Syntax of TypeScript Interfaces
The syntax of a TypeScript interface is straightforward:

interface InterfaceName {
    property1: type;
    property2: type;
    // Additional properties and methods can be defined here
}
Enter fullscreen mode Exit fullscreen mode

Here’s a breakdown of the syntax elements:

  • interface: Keyword used to define an interface.
  • InterfaceName: Name of the interface following TypeScript naming conventions.
  • property1, property2: Properties of the interface.
  • type: TypeScript type annotation defining the type of each property(String, Number, Boolean, Object Type, Array, Tuple, Enum, Any , Unknown, Void, …)

Let’s start with a simple example:
Defining an Interface
In TypeScript, there are two ways to define an interface. The first method is to define it inline for an object. Another method is to create a separate interface and assign it to any object.

function getProductInfo(product: {name: string, price: number}): string {
 return `${product.name}: ${product.price} `;
}
const product = {
 name: "Laptop",
 price: 999.99
};
console.log(getProductInfo(product));
Enter fullscreen mode Exit fullscreen mode

Output:

Laptop: 999.99

Enter fullscreen mode Exit fullscreen mode

The following uses an interface Product that has two properties a string and another number:

interface Product {
    name: string;
    price: number;
}
Enter fullscreen mode Exit fullscreen mode

By convention, the interface names are in the PascalCase. They use a single capitalized letter to separate words in their names. For example, Product, Person, UserProfile, and FullName.
After defining the Person, interface, you can use it as a type. For example, you can annotate the function parameter with the interface name:

function getProductInfo(product: Product) {
    return `${product.name}: ${product.price} `;
}

const product = {
 name: "Laptop",
 price: 999.99
};

console.log(getProductInfo(product));
Enter fullscreen mode Exit fullscreen mode

The code now is easier to read than before.

To make the code more concise, you can use the object destructuring feature of JavaScript:

function getProductInfo({ name, price }: Product) {
  return `${name}: ${price} `;
}
Enter fullscreen mode Exit fullscreen mode

In the argument, we destructure the properties of the product object:

{ name, price }: Product

Enter fullscreen mode Exit fullscreen mode

The getProductInfo() function will accept any object that has at least two string properties with the name name and price.

For example, the following code declares an object that has six properties:

let product = {
  name: "Laptop",
  price: 999.99
  category: "Electronics",
  description: "High-performance laptop with 16GB RAM",
  weight: 2000,
  stock: 5
};
Enter fullscreen mode Exit fullscreen mode

Since the product object has two properties name and price, you can pass it on to the getProductInfo() function as follows:

let productInfo= getProductInfo(product);
console.log(productInfo); 
Enter fullscreen mode Exit fullscreen mode

output

Laptop: 999.99
Enter fullscreen mode Exit fullscreen mode

Example in frontend:
🔵 React Example (with TypeScript):

// Product.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  images: string[];
  stock: number;
  category: string;
}

// ProductDisplay.tsx
const ProductDisplay: React.FC<{ product: Product }> = ({ product }) => {
  return (
    <div className="product-card">
      <img 
        src={product.images[0]} 
        alt={product.name}
        className="product-image"
      />
      <h2 className="product-name">{product.name}</h2>
      <p className="product-description">{product.description}</p>
      <div className="product-details">
        <span className="product-price">${product.price.toFixed(2)}</span>
        <span className="product-stock">
          {product.stock > 0 ? 'In Stock' : 'Out of Stock'}
        </span>
      </div>
    </div>
  );
};

// Usage in App.tsx
import React from 'react';
import { ProductDisplay } from './ProductDisplay';

const App = () => {
  const product: Product = {
    id: "PROD001",
    name: "Premium Laptop",
    price: 999.99,
    description: "High-performance laptop with 16GB RAM and 512GB SSD",
    images: ["laptop1.jpg", "laptop2.jpg"],
    stock: 5,
    category: "Electronics"
  };

  return (
    <div className="app">
      <h1>Product Details</h1>
      <ProductDisplay product={product} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

🔴 Angular Example (with TypeScript):

// product.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  images: string[];
  stock: number;
  category: string;
}

// product-display.component.ts
import { Component, Input } from '@angular/core';
import { Product } from './product';

@Component({
  selector: 'app-product-display',
  template: `
    <div class="product-card">
      <img [src]="product.images[0]" [alt]="product.name" class="product-image">
      <h2 class="product-name">{{ product.name }}</h2>
      <p class="product-description">{{ product.description }}</p>
      <div class="product-details">
        <span class="product-price">${{ product.price | number:'1.2-2' }}</span>
        <span class="product-stock">
          {{ product.stock > 0 ? 'In Stock' : 'Out of Stock' }}
        </span>
      </div>
    </div>
  `
})
export class ProductDisplayComponent {
  @Input() product!: Product;
}

// app.component.ts
import { Component } from '@angular/core';
import { Product } from './product';

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      <h1>Product Details</h1>
      <app-product-display [product]="currentProduct"></app-product-display>
    </div>
  `
})
export class AppComponent {
  currentProduct: Product = {
    id: "PROD001",
    name: "Premium Laptop",
    price: 999.99,
    description: "High-performance laptop with 16GB RAM and 512GB SSD",
    images: ["laptop1.jpg", "laptop2.jpg"],
    stock: 5,
    category: "Electronics"
  };
}

// product.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductDisplayComponent } from './product-display.component';

@NgModule({
  declarations: [ProductDisplayComponent],
  imports: [CommonModule],
  exports: [ProductDisplayComponent]
})
export class ProductModule { }

Enter fullscreen mode Exit fullscreen mode

🟢 Vue 3 Example (with TypeScript + Composition API):

// Product.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  images: string[];
  stock: number;
  category: string;
}
Enter fullscreen mode Exit fullscreen mode
<!-- ProductDisplay.vue -->
<template>
  <div class="product-card">
    <img :src="product.images[0]" :alt="product.name" class="product-image">
    <h2 class="product-name">{{ product.name }}</h2>
    <p class="product-description">{{ product.description }}</p>
    <div class="product-details">
      <span class="product-price">${{ product.price.toFixed(2) }}</span>
      <span class="product-stock">
        {{ product.stock > 0 ? 'In Stock' : 'Out of Stock' }}
      </span>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { defineProps } from 'vue';
import type { Product } from './Product';

const props = defineProps<{ product: Product }>();
</script>
Enter fullscreen mode Exit fullscreen mode
<!-- App.vue -->
<template>
  <div class="app">
    <h1>Product Details</h1>
    <ProductDisplay :product="currentProduct" />
  </div>
</template>

<script lang="ts" setup>
import ProductDisplay from './ProductDisplay.vue';
import type { Product } from './Product';

const currentProduct: Product = {
  id: "PROD001",
  name: "Premium Laptop",
  price: 999.99,
  description: "High-performance laptop with 16GB RAM and 512GB SSD",
  images: ["laptop1.jpg", "laptop2.jpg"],
  stock: 5,
  category: "Electronics"
};
</script>

<!-- main.ts -->
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');
Enter fullscreen mode Exit fullscreen mode

**Example in backend:
📁 Backend Setup (Node.js + Express + TypeScript):

  1. Person Interface (User.ts)**
export interface User {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  isActive: boolean;
}

Enter fullscreen mode Exit fullscreen mode

** 2. Server (server.ts) **

import express from 'express';
import { Person } from './Person';

const app = express();
const port = 3000;

app.use(express.json());

let people: Person[] = [
  { id: 1, firstName: 'John', lastName: 'Doe' },
  { id: 2, firstName: 'Jane', lastName: 'Smith' },
];

// GET all people
app.get('/people', (_req, res) => {
  res.json(people);
});

// GET one person
app.get('/people/:id', (req, res) => {
  const person = people.find(p => p.id === parseInt(req.params.id));
  if (!person) return res.status(404).send('Not found');
  res.json(person);
});

// POST create person
app.post('/people', (req, res) => {
  const { firstName, lastName } = req.body;
  const newPerson: Person = {
    id: people.length + 1,
    firstName,
    lastName
  };
  people.push(newPerson);
  res.status(201).json(newPerson);
});

// DELETE person
app.delete('/people/:id', (req, res) => {
  people = people.filter(p => p.id !== parseInt(req.params.id));
  res.status(204).send();
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Optional Properties
We can make properties optional by adding a question mark (?) after the property name, Optional properties allow us to define interface properties that are not required to be implemented. This provides flexibility in implementing the interface.If we do not implement any of the properties, we will not receive any errors.

interface PersonInterface {
    name?: string;
    age?: number;
    address?: string;
}
Enter fullscreen mode Exit fullscreen mode
class Person implements PersonInterface {
    name: string;
    constructor(name: string, age?: number, address?: string) {
        this.name = name;
        if (age) {
            console.log(`Age: ${age}`);
        }
        if (address) {
            console.log(`Address: ${address}`);
        }
    }
}
const person1 = new Person("Alice");
const person2 = new Person("Bob", 30);
const person3 = new Person("Charlie", 25, "123 Main St");
Enter fullscreen mode Exit fullscreen mode

output

Alice
Bob - Age: 30
Charlie - Age: 25, Address: 123 Main St
Enter fullscreen mode Exit fullscreen mode

In this example, age and address are optional. The Person class can choose to implement none, some, or all of these properties without causing any TypeScript errors.

Optional and Readonly Properties
Optional (?) and readonly properties in interfaces provide flexibility and control by allowing properties to be optional or immutable after initialization. This enhances type safety and prevents unintended modification

interface Product {
  id: number;
  name: string;
  description?: string;
  readonly price: number;
}
Enter fullscreen mode Exit fullscreen mode
const product: Product = { id: 1, name: 'Laptop', price: 1500 };

Enter fullscreen mode Exit fullscreen mode
product.price = 1600; 
// Error: Cannot assign to 'price' because it is a read-only property
Enter fullscreen mode Exit fullscreen mode

Implementing Interfaces in Classes
Classes can also implement these interfaces, meaning they must define all the properties specified in the interface:

interface ProductInterface {
    name: string;
    price: number;
}
class Product implements ProductInterface {
    name: string;
    price: number;
}

Enter fullscreen mode Exit fullscreen mode

Additionally, we can define methods in interfaces as well:

interface ProductInterface {
    name: string;
    price: number;
    currency: string;
    discountPercentage?: number;

    setName(name: string): void;
    setPrice(price: number, currency: string): void;
    applyDiscount(percentage: number): void;
    calculateFinalPrice(): number;
}

class Product implements ProductInterface {
    name: string;
    price: number;
    currency: string;
    discountPercentage: number = 0;

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

    setName(name: string): void {
        this.name = name;
    }

    setPrice(price: number, currency: string): void {
        this.price = price;
        this.currency = currency;
    }

    applyDiscount(percentage: number): void {
        if (percentage >= 0 && percentage <= 100) {
            this.discountPercentage = percentage;
        } else {
            throw new Error('Discount percentage must be between 0 and 100');
        }
    }

    calculateFinalPrice(): number {
        const discountAmount = (this.price * this.discountPercentage) / 100;
        return this.price - discountAmount;
    }
}

Enter fullscreen mode Exit fullscreen mode

✅ 1. Multiple Interface Implementation:
A class can implement multiple interfaces to combine functionality:

interface Nameable {
    name: string;
    setName(name: string): void;
}

interface Pricable {
    price: number;
    setPrice(price: number): void;
}

class Product implements Nameable, Pricable {
    name: string;
    price: number;

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

    setName(name: string): void {
        this.name = name;
    }

    setPrice(price: number): void {
        this.price = price;
    }
}

const item = new Product("Laptop", 1000);
item.setName("Gaming Laptop");
item.setPrice(1200);

console.log(item);

Enter fullscreen mode Exit fullscreen mode

output

Product { name: 'Gaming Laptop', price: 1200 }
Enter fullscreen mode Exit fullscreen mode

✅ 2. Implementing Interface for a Service Class:
Interfaces are not just for data structures but also for defining the structure of service classes:

interface ProductServiceInterface {
    getProducts(): Product[];
    addProduct(product: Product): void;
}

class Product {
    constructor(public name: string, public price: number) {}
}

class ProductService implements ProductServiceInterface {
    private products: Product[] = [];

    getProducts(): Product[] {
        return this.products;
    }

    addProduct(product: Product): void {
        this.products.push(product);
    }
}

const service = new ProductService();
service.addProduct(new Product("Book", 20));
console.log(service.getProducts());
Enter fullscreen mode Exit fullscreen mode

output:

[ Product { name: 'Book', price: 20 } ]

Enter fullscreen mode Exit fullscreen mode

✅ 3. Combining Interfaces (Interface Composition):
We can combine multiple interfaces in a class:

interface Identifiable {
    id: number;
}

interface Timestamped {
    createdAt: Date;
    updatedAt: Date;
}

class AuditableProductImpl implements Identifiable, Timestamped {
    id: number;
    createdAt: Date;
    updatedAt: Date;

    constructor(id: number) {
        this.id = id;
        this.createdAt = new Date();
        this.updatedAt = new Date();
    }

    update(): void {
        this.updatedAt = new Date();
    }
}

class ProductImpl extends AuditableProductImpl {
    name: string;
    price: number;
    currency: string;
    discountPercentage: number = 0;

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

    setName(name: string): void { this.name = name; }
    setPrice(price: number, currency: string): void {
        this.price = price;
        this.currency = currency;
    }
    applyDiscount(percentage: number): void {
        this.discountPercentage = percentage;
    }
    calculateFinalPrice(): number {
        return this.price - (this.price * this.discountPercentage) / 100;
    }
}

Enter fullscreen mode Exit fullscreen mode

In this example, we demonstrate how to combine multiple interfaces (Identifiable and Timestamped) to form a more complex class structure. The AuditableProductImpl class implements both interfaces, ensuring that each instance has id, createdAt, and updatedAt properties. The ProductImpl class extends AuditableProductImpl to include additional product-specific properties (name, price, currency, etc.) and methods (setName, setPrice, applyDiscount, and calculateFinalPrice). This approach promotes modular design and enforces consistent structure across related classes.

✅ 4. Dynamic Implementation with Factory Pattern:

interface Notification {
    send(message: string): void;
}

class EmailNotification implements Notification {
    send(message: string): void {
        console.log(`Sending email with message: ${message}`);
    }
}

class SMSNotification implements Notification {
    send(message: string): void {
        console.log(`Sending SMS with message: ${message}`);
    }
}

class NotificationFactory {
    static createNotification(type: "email" | "sms"): Notification {
        if (type === "email") {
            return new EmailNotification();
        } else {
            return new SMSNotification();
        }
    }
}

const emailService = NotificationFactory.createNotification("email");
emailService.send("Hello via Email!");

const smsService = NotificationFactory.createNotification("sms");
smsService.send("Hello via SMS!");

Enter fullscreen mode Exit fullscreen mode

In this example, we implement the Factory Pattern to dynamically create instances of different notification classes (EmailNotification and SMSNotification) that implement the Notification interface. The NotificationFactory class provides a single method, createNotification, which accepts a type ("email" or "sms") and returns the appropriate notification instance. This approach decouples the client code from the specific implementations and allows for easy extension and maintenance of new notification types without modifying existing logic.

Extending Interfaces
Extending interfaces in TypeScript allows one interface to inherit the properties and methods of another, enabling code reuse and building on existing type definitions. This promotes modularity and flexibility in type design.

interface Person {
  firstName: string;
  lastName: string;
}
interface Employee extends Person {
  employeeId: number;
  department: string;
}
const employee: Employee = {
  firstName: 'Charlie',
  lastName: 'Brown',
  employeeId: 123,
  department: 'Engineering'
};

Enter fullscreen mode Exit fullscreen mode

In this example, the Employee interface extends the Person interface, meaning it inherits the firstName and lastName properties from Person while adding its own properties: employeeId and department.

  • The Person interface defines basic personal information (firstName and lastName).
  • The Employee interface extends Person and adds additional properties specific to employees (employeeId and department).

The employee object implements the Employee interface, which means it must include all properties from both Person and Employee, resulting in a complete object with firstName, lastName, employeeId, and department.

This technique allows for reusability and extension of existing interfaces, promoting clean and scalable type design.

another example:

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

interface RegisterInterface extends PersonInterface {
    setName(name: string): void;
    getName(): string;
}

class Person implements RegisterInterface {
    name: string;
    age: number;

    public setName(name: string): void {
        this.name = name;
    }

    public getName(): string {
        return this.name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)