Why You Should Avoid Using the 'I' Prefix for Interfaces in TypeScript

mscamargo

Marcos S. Camargo

Posted on August 10, 2024

Why You Should Avoid Using the 'I' Prefix for Interfaces in TypeScript

In the world of software development, naming conventions often spark debate. One such convention is the use of the "I" prefix for interfaces, a practice that has found its way into TypeScript from languages like C# and Java. While it might seem harmless at first glance, this convention can lead to unnecessary complexity and confusion, especially when applied to TypeScript. In this article, we'll explore why you should avoid using the "I" prefix for interfaces and how adhering to clear, meaningful naming practices can improve your codebase.

The Origin of the "I" Prefix

The "I" prefix convention comes from languages like C# and Java, where it was commonly used to distinguish interfaces from concrete classes. For instance, in C#, you might find a class named UserService and an interface named IUserService. The intention behind this convention was to make it immediately clear that IUserService was an interface, not a concrete implementation.

While this made sense in the context of those languages, where interfaces and classes often coexist in complex hierarchies, it doesn't always translate well to TypeScript, a language designed to be more flexible and less ceremonious.

The Case Against the "I" Prefix in TypeScript

1. Unnecessary Abstraction

One of the key principles of good software design is to avoid unnecessary abstraction. In TypeScript, interfaces are often used to define the shape of an object or the contract a class must adhere to. However, prefixing every interface with "I" can lead to redundant abstractions, particularly when there is only one implementation.

For example, consider a scenario where you have an interface called IUser and a single implementation class called User. This can lead to confusion and unnecessary verbosity:

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

class User implements IUser {
  constructor(public name: string, public age: number) {}
}
Enter fullscreen mode Exit fullscreen mode

In this case, the "I" prefix doesn't add any real value. Instead, it creates a layer of abstraction that is more ceremonial than functional. By simply naming the interface User, you maintain clarity and simplicity:

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

class Person implements User {
  constructor(public name: string, public age: number) {}
}
Enter fullscreen mode Exit fullscreen mode

2. Meaningful Naming Leads to Better Design

Let's consider a more complex example involving a caching system. Suppose you're designing an interface for a cache manager. Using the "I" prefix, you might end up with something like ICacheManager for the interface and CacheManager for the concrete implementation. However, a more meaningful approach would be to name the interface CacheManager and provide specific implementations like RedisCacheManager, MemoryCacheManager, or FileCacheManager.

Here's how this might look:

interface CacheManager {
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<void>;
  delete(key: string): Promise<void>;
}

class RedisCacheManager implements CacheManager {
  // Implementation using Redis
  async get(key: string): Promise<string | null> {
    // Redis get operation
  }

  async set(key: string, value: string): Promise<void> {
    // Redis set operation
  }

  async delete(key: string): Promise<void> {
    // Redis delete operation
  }
}

class MemoryCacheManager implements CacheManager {
  // Implementation using in-memory storage
  private cache: Map<string, string> = new Map();

  async get(key: string): Promise<string | null> {
    return this.cache.get(key) || null;
  }

  async set(key: string, value: string): Promise<void> {
    this.cache.set(key, value);
  }

  async delete(key: string): Promise<void> {
    this.cache.delete(key);
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach provides several benefits:

  • Clarity: The name CacheManager clearly indicates the role of the interface, while RedisCacheManager and MemoryCacheManager explicitly convey what each implementation does.
  • Flexibility: You can easily add new implementations (e.g., FileCacheManager) without changing the interface or the existing code that depends on it.
  • Alignment with Interface Segregation Principle: By avoiding the "I" prefix and focusing on meaningful names, you naturally align with the Interface Segregation Principle (ISP), one of the SOLID principles of object-oriented design. ISP suggests that clients should not be forced to depend on interfaces they do not use. In other words, interfaces should be focused and specific, which is more easily achieved when they are named meaningfully rather than generically.

3. Prefixing Can Lead to Violations of Interface Segregation Principle

Using the "I" prefix often leads developers down a path where they create large, catch-all interfaces that do not respect the Interface Segregation Principle. For example, an interface like ICacheManager might be tempted to include methods for all possible caching operations, leading to a bloated and unwieldy contract:

interface ICacheManager {
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<void>;
  delete(key: string): Promise<void>;
  clear(): Promise<void>;
  keys(): Promise<string[]>;
}
Enter fullscreen mode Exit fullscreen mode

In contrast, if you avoid the "I" prefix and instead focus on more specific, meaningful interfaces, you're more likely to adhere to ISP and create smaller, more focused interfaces:

interface CacheReader {
  get(key: string): Promise<string | null>;
}

interface CacheWriter {
  set(key: string, value: string): Promise<void>;
}

interface CacheRemover {
  delete(key: string): Promise<void>;
}

class RedisCacheManager implements CacheReader, CacheWriter, CacheRemover {
  // Implementation using Redis
}
Enter fullscreen mode Exit fullscreen mode

This design is more modular, easier to understand, and allows for greater flexibility in how you implement caching logic.

4. Consistency Across Types

If you decide to use the "I" prefix for interfaces, consistency would demand similar prefixes for other types, such as "E" for enums, "T" for types, "C" for classes, and so on. This quickly becomes cumbersome and counterproductive.

Imagine a codebase where every enum is prefixed with "E" and every type with "T":

enum EUserRole {
  Admin,
  User,
  Guest
}

type TUser = {
  name: string;
  age: number;
  role: EUserRole;
}

class CUser implements IUser {
  constructor(public name: string, public age: number, public role: EUserRole) {}
}
Enter fullscreen mode Exit fullscreen mode

This level of prefixing adds unnecessary noise to the code and can make it harder to read and maintain. The essence of good naming conventions is to create meaningful, self-explanatory names, not to burden them with redundant prefixes.

5. Modern IDEs and TypeScript's Flexibility

One of the reasons for the "I" prefix in languages like C# was the lack of strong tooling support in the early days. Developers needed a quick way to distinguish between interfaces and classes without relying on external tools. Today, modern IDEs and editors offer robust support for TypeScript, including features like intellisense, type inference, and quick lookups. This makes the "I" prefix unnecessary, as the tools themselves provide enough context.

6. Real-World Example: Redux Toolkit

Let's take a look at a real-world example from the Redux Toolkit, a popular library for managing state in React applications. In the Redux Toolkit, you'll often see interfaces and types defined without the "I" prefix, focusing instead on clear and descriptive names:

interface RootState {
  user: UserState;
  posts: PostsState;
}

interface UserState {
  id: string;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

Here, the names are straightforward and easy to understand. The absence of the "I" prefix doesn't detract from the clarity; in fact, it enhances it by keeping the code clean and concise.

Conclusion

The "I" prefix for interfaces in TypeScript is a convention that has outlived its usefulness. In modern TypeScript development, where tools and practices have evolved, this prefix often leads to unnecessary abstraction, inconsistency, and clutter. Instead, focus on naming interfaces, types, and classes in a way that clearly conveys their purpose without relying on redundant prefixes.

Moreover, by avoiding the "I" prefix and embracing meaningful, descriptive names, you'll naturally align your code with key principles of good software design, such as the Interface Segregation Principle. This leads to code that is not only easier to read and maintain but also more flexible and scalable.

In the end, the goal of good software design is not just to follow conventions, but to create code that is as simple, clear, and maintainable as possible. By rethinking the use of the "I" prefix, you can take a step towards cleaner, more efficient TypeScript code that better serves both your team and your users.

This is an experimental article co-written with ChatGPT.

💖 💪 🙅 🚩
mscamargo
Marcos S. Camargo

Posted on August 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related