Favour TypeScript Types Over Interfaces
https://www.lloydatkinson.net/posts/2023/favour-typescript-types-over-interfaces/
Some time ago, I came to the conclusion that the TypeScript type
keyword should almost always be favoured over the interface
keyword. I arrived at this conclusion after considering the use cases and implications. Of course, there is a large overlap, but I personally believe that interfaces (in TypeScript) are a more restrictive and less useful construct than types.
Overlapping functionality
When I first used TypeScript, I was surprised that two language features could be so similar as to appear almost indistinguishable.
- Both allow for declaring a contract or shape of a type1
- Both can be members of a union type
- Both can be extended to create derived types
- Both can be used for OO inheritance with classes and the
implements
keyword
Types are more widely applicable
The type construct is more general purpose than interfaces. Consider the following code block.
type Status = "standard" | "vip"
type User = { name: string; status: Status }
Not only was a contract or shape of a user-defined, but a union type was also defined. A clear advantage is revealed; types can express multiple constructs and are more versatile than interfaces.
Union types are also explicitly part of the syntax, such as string | number.
That is, types are a more natural and everyday occurrence in TypeScript. For example, consider the following code block.
// This is not valid.
// interface Status = 'standard' | 'vip';
type Status = 'standard' | 'vip';
interface User = { name: string, status: Status };
What has been achieved here compared to the first code block, other than extra characters and using two keywords and language features over one? Not much - other than confusion when revisiting the codebase and raising questions as to why both were used.
Using types is more consistent
The previous section demonstrates that types are less restrictive and surpass the use cases of interfaces. Continuing to use both keywords is simply inconsistent for no good reason. Making the choice between them is not difficult. The stricter but more versatile keyword or the looser and less useful keyword?
Type
wins.
Interfaces can be merged (that’s rarely good)
Interfaces support declaration merging. This is definetly a “feature” to be cautious of. It has two good use cases - creating or extending the typings of third party libraries and declaring environment variables.
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PLAUSIBLE_API_KEY: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Outside of those two use cases, I do not see any value in the following code block. It’s simply confusing, especially if multiple interfaces with the same name are created throughout the code base.
interface Box {
height: number
width: number
}
interface Box {
scale: number
}
const box: Box = { height: 5, width: 6, scale: 10 }
Interfaces lean more towards OO thinking
Interfaces are, by their nature, strongly tied to classes. I would even suggest that interfaces were added to TypeScript specifically to support the class-based OO paradigm.
To be clear, I’m not saying OO is bad (though I often favour functional concepts), but let’s be realistic - the TypeScript (and JavaScript) ecosystem leans strongly towards approximately two paradigms: non-class based object orientation and functional.
I can only think of two examples where I used classes or OO with TypeScript recently. This strongly contrasts with a typical project using UI libraries like React, where simple functions + data (often in the form of hooks) and declarative patterns and concepts are commonplace2.
There are some common myths used to “sell” TypeScript to developers. I believe some of these myths are harmful and perpetuate this “let’s apply OO everywhere without considering the implications of the ecosystem”. One of these is that because TypeScript has classes, it is an OO language - it’s a multi-paradigm language. Aside from the fact that JavaScript also has classes, this myth is popular amongst developers who have only experienced OO.
This is a topic I’d like to expand on more in another article, so I’ll finish this section with an insightful talk from Douglas Crockford.
There are a few we know are gonna be bad. The worst is class. Class was the most requested new feature in JavaScript and all of the requests came from Java programmers who have to program in JavaScript and don’t want to have to learn how to do that.
Types can still be used with classes
Again demonstrating the versatility of types, I created a customer processor in a typical OO style. There are two types available for implementation, and both are examples of higher-order functions (or in OO terminology: dependency injection). No interfaces here, yet still featuring composition.
type Trackable = {
onProgress: ((progress: number) => void) | null
}
type CustomerProcessor = {
processBatch(customers: Customer[]): void
}
class CustomerProcessorWithProgress implements CustomerProcessor, Trackable {
private processed: number = 0
onProgress: ((progress: number) => void) | null = null
processBatch(customers: Customer[]) {
this.processed = this.processed + customers.length
this.onProgress?.(this.processed)
}
}
Playground Link (full version)
Interface Hungarian Notation
Developers often bring conventions, patterns, and idioms from other languages they know. This can be good, as functional concepts like immutability, pure functions, and composition over inheritance work well in OO languages.
However, using naming conventions from one language in definitely not something to bring across. For example, C# interfaces starting with “I” are not idiomatic in TypeScript, but they are idiomatic C#. The point of a naming convention is to make code more readable and consistent.
Conclusion
I’ve explained my preference and why I believe type should be the default choice in your projects. It’s up to you which you use, but I hope you can see the benefits of types and why they are a better choice in most cases.
The inevitable angry comments can be directed to /dev/null
.
Footnotes
The term “types” in TypeScript can refer to any object, be it a type alias or an interface. However, in the TypeScript ecosystem, the word “type” is typically usually used to specifically refer to the
type
keyword. This distinction is generally understood, even amongst pedants who are particular about precise language usage˄Angular being the opposite and the framework of choice for the “I don’t want to learn JavaScript/TypeScript” crowd.˄