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) {}
}
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) {}
}
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);
}
}
This approach provides several benefits:
-
Clarity: The name
CacheManager
clearly indicates the role of the interface, whileRedisCacheManager
andMemoryCacheManager
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[]>;
}
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
}
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) {}
}
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;
}
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.
Top comments (1)
Gonna be real here, these are all great examples of how to write better interfaces, but absolutely none of this is an argument for dropping the I from the prefix. Your example of IUser -> User is bad because now you have to name your class Person, and is Person a User? In my opinion you have now introduced even more confusion and bad design in your attempt to prove that dropping I is better. You can have a DatabaseCacheManager and a MemoryCacheManager that both implement from ICacheManager just as easily as you can have it implement from CacheManager. You can have IReadCache, IWriteCache just as easily as you wrote ReachCache, WriteCache.