Modern Naming Conventions in TypeScript: Avoiding the I Prefix and Interface Suffix

Published on

4 min read

Categories associated to this post are: TypeScript

Naming conventions are essential for writing clean, readable TypeScript code. An outdated practice, inherited from languages like Java and C#, is to use an I prefix or Interface suffix to differentiate interfaces from classes. In TypeScript, these markers are unnecessary and can make code harder to read. In this post, we’ll explore a cleaner, more modern approach to naming conventions, even when working with scenarios that involve multiple implementations of a single type.

The I Prefix and Interface Suffix: Outdated and Unnecessary

In Java and C#, developers commonly used an I prefix (e.g., IProduct) or an Interface suffix to distinguish interfaces from concrete classes. This convention was useful for strongly typed, class-based languages. TypeScript, however, is structurally typed, meaning its types are defined by structure rather than explicit markers. In TypeScript, these extra markers don’t add clarity and instead make the code noisier.

Example: Old Naming Convention

Let’s start with an example of the old naming convention using Product and Order types:

TypeScript
// Outdated Convention
interface IProduct {
  id: number;
  name: string;
}
 
interface IOrder {
  orderId: number;
  product: IProduct;
  quantity: number;
}

While this code is perfectly valid, the I prefix on IProduct and IOrder doesn’t contribute to readability. Instead, it introduces unnecessary noise into the code.

Modern Naming Convention: Descriptive and Concise

A more modern approach in TypeScript is to use simple, descriptive names without prefixes or suffixes. The names Product and Order are straightforward and make the code more readable.

TypeScript
// Modern Convention
type Product = {
  id: number;
  name: string;
};
 
type Order = {
  orderId: number;
  product: Product;
  quantity: number;
};

This code is both cleaner and easier to understand. The use of Product and Order as type names provides all the information a developer needs without any extraneous markers.

Why Prefer Type Aliases Over Interfaces?

TypeScript provides both interface and type keywords for defining types. While you could mix them, it’s generally better to pick one for consistency. Many TypeScript engineers prefer using type for its flexibility. With type, you can define unions, intersections, and mapped types, which aren’t possible with interface.

Key Benefits of Type Aliases

  • Union Types: Type aliases allow for unions, such as type Status = 'pending' | 'completed';.
  • Mapped Types: You can create mapped types like type ReadOnly<T> = { readonly [K in keyof T]: T[K] };.
  • Intersections: Type aliases support intersections, making them flexible for combining types, such as type Item = Product & Order;.

By using type consistently, we create a predictable structure in our code. It’s easy to add or modify types without mixing keywords, which keeps our codebase readable and organized.

Real-World Example: Consistent Naming with Multiple Implementations

To see how this approach applies in a real-world scenario, let’s imagine we have a DataRepository type with different implementations for different data sources, such as Redis and PostgreSQL. Instead of using IDataRepository, we can name the type DataRepository and name each implementation based on the data source it represents. Here’s how it might look in practice:

TypeScript
// Define the shared type
type DataRepository = {
  fetchData(): Promise<string>;
};
 
// Concrete implementations
class RedisDataRepository implements DataRepository {
  async fetchData() {
    return "Fetched data from Redis";
  }
}
 
class PostgreDataRepository implements DataRepository {
  async fetchData() {
    return "Fetched data from PostgreSQL";
  }
}
 
// Example function using DataRepository without worrying about the specific implementation
const processData = async (repository: DataRepository) => {
  const data = await repository.fetchData();
 
  console.log("Processing:", data);
};
 
// Using the function with different implementations
const redisRepo = new RedisDataRepository();
const postgreRepo = new PostgreDataRepository();
 
processData(redisRepo);    // Logs: "Processing: Fetched data from Redis"
processData(postgreRepo);  // Logs: "Processing: Fetched data from PostgreSQL"

In this example, DataRepository is the base type that any implementation (e.g., RedisDataRepository, PostgreDataRepository) can follow. This naming approach makes it easy to understand each implementation without extra prefixes or suffixes. Each class name is descriptive, indicating both the purpose (DataRepository) and the specific data source (e.g., Redis, PostgreSQL), making the code clearer and easier to maintain.

Summary: Clean Code Through Consistent Naming

Modern TypeScript doesn’t require outdated naming conventions like I prefixes or Interface suffixes. By using simple, descriptive names, you can make your code cleaner and more readable. Using type aliases consistently and choosing meaningful names, such as Product, Order, or DataRepository, helps maintain a codebase that is both intuitive and adaptable to modern TypeScript practices.

Share on social media platforms.