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.
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
}
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));
Output:
Laptop: 999.99
The following uses an interface Product that has two properties a string and another number:
interface Product {
name: string;
price: number;
}
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));
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} `;
}
In the argument, we destructure the properties of the product object:
{ name, price }: Product
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
};
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);
output
Laptop: 999.99
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;
🔴 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 { }
🟢 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;
}
<!-- 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>
<!-- 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');
**Example in backend:
📁 Backend Setup (Node.js + Express + TypeScript):
- Person Interface (User.ts)**
export interface User {
id: number;
firstName: string;
lastName: string;
email: string;
age: number;
isActive: boolean;
}
** 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}`);
});
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;
}
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");
output
Alice
Bob - Age: 30
Charlie - Age: 25, Address: 123 Main St
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;
}
const product: Product = { id: 1, name: 'Laptop', price: 1500 };
product.price = 1600;
// Error: Cannot assign to 'price' because it is a read-only property
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;
}
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;
}
}
✅ 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);
output
Product { name: 'Gaming Laptop', price: 1200 }
✅ 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());
output:
[ Product { name: 'Book', price: 20 } ]
✅ 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;
}
}
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!");
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'
};
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;
}
}
Top comments (0)