As you are reading this, I’m going to assume that you have a basic idea of what TypeScript is (which is basically JavaScript with syntax for types, according to the official docs). The purpose of this piece of writing is to discuss and look into the contrast between two commonly used constructs widely used in TypeScript: Type, and Interface.
In short, we use type as type aliases, which is, a name for any type. With the help of this, we can use the same type more than once and refer to it by a single name. On the other hand, interface, or interface declaration is basically another way to name an object type, actually a contract for a structure.
Now, they may not seem very similar by the definition. But here we are here, talking about what are the differences, and maybe trying to choose only one between type and interface, there should be some similarities that might make you confused, right?
In a bunch of cases, we can use type and interface interchangeably, and choose them freely. For object shapes and function signatures, both can work the same way. Let’s see an example. We will define objects and functions using both type and interface.
// defining object
type ObjectType = {
name: string;
author: number;
fun(): void;
}
let thrillerBook: ObjectType;
interface ObjectInterface {
name: string;
author: number;
fun(): void;
}
let fantasyBook: ObjectInterface;
// defining function
type FunctionType = (author: string) => string;
const getAcademicAuthor: FunctionType = (author) => {
return `The academic author is ${author}`;
}
interface FunctionInterface {
(author: string): string;
}
const getFictionAuthor: FunctionInterface = (author) => {
return `The fiction author is ${author}`;
}
But yes, there are some obvious differences. Both of the keywords has some superpowers of their own, that the other one does not have. Let’s dive into those.
Superpowers of type
You can use type to represent anything from primitives, unions, tuples, and various complex type. But interface is mostly limited to defining shapes e. g. objects (as well as callable & constructable signatures).
type Title = string; // primitive
type Published = string | number; // union
type Binding = [string, string]; // tuples
type Spine = "flat" | "round"; // template literal
type ReadonlyBook = {
readonly [K in keyof Book]: Book[K];
} // mapped types
There are cases, when we must fall back to using type alias. We can roughly list them down like this:
- Union types
- Intersection types
- Mapped types
- Conditional types
- Template literal types
These are some mere examples that interface does not support. It can be used for far more complex typing.
Superpowers of interface
While using interface for defining shapes or structure, there are several perks over using type. One of the major distinctions between type and interface is that type cannot be re-opened and redeclared, whereas interface can be. We call this declaration merging.
interface Book {
title: string;
author: string;
}
interface Book {
published: number | string;
reprint(): void;
}
// we can do this, no error.
Now, if we declare an object of type Book, it will have all the properties from both definitions, i. e. title, author, published, reprint(). But such is not the case for type.
type Movie = {
title: string;
released: number;
}
type Movie = {
director: string;
}
// we cannot do this, TS will throw error.
To achieve the same with type, we must define a new type, and intersect the old one with it. Something like this:
type MovieWithDirector = Movie & {
director: string;
}
But this method has a little overhead comparing to the one we saw earlier with declaration merging.
Even though declaration merging is one of the biggest superpowers of interface, it comes with a warning label—overusing merging can make the codebase harder to reason about.
Extensions and Implementations
An interface can extend both interface, and a type. Remember the Book interface and the Movie type we defined earlier in the example? Let’s extend those:
// interface extends another interface
interface PaperbackBook extends Book {
binding: "paperback";
}
// interface extends another type alias
interface AnimatedMovie extends Movie {
medium: string;
}
But remember, for an interface to be able to extend a type alias, it must be of an object type. For example, the below extension is not possible.
type Magic = number | string | [string, string] | number[];
interface Technology extends Magic {
...
}
// this will throw an error
The bottom line is, interfaces can extend other interfaces and sometimes type aliases if they define shapes. But the opposite is not possible in the same way. For that, we have to use intersection, as we did before to mock declaration merging with type aliases.
This scenario remains the same if a class tries to implement either of these. Let’s continue with the previously declared Book interface and Movie type.
// a class implements an interface
class FantasyBook implements Book {
title = "The Hobbit";
author = "John Ronald Reuel Tolkien";
published = 1937;
reprint() {
console.log("reprint this again!");
};
}
// a class implements a type alias
class AnimatedMovie implements Movie {
title = "Tarzan";
released = 1999;
isFavouriteMovie = true;
}
But, same as before, the type alias must define a shape i.e. an object. Otherwise, it cannot be implemented.
type NoShape = number | string | [string, string] | number[];
class Shape implements NoShape {
...
}
// this will throw an error
So, for its very nature, interface can be extended or implemented however we like, without any worries, as they define a shape. But as type alias is more of a broader toolbox for type definition, there are some limitations and cases where we need to be careful.
Now, the end is nigh, and I shall conclude this with a million dollar question:
Type or Interface, which one to use?
As both type and interface share some common use cases, the question is: which one to use?
Well, actually, there is no obvious choice. At the end, it really depends on you. But before deciding that, here are some facts:
- TypeScript provides better and more focused error messages while working with interface.
- Interface was supposed to be slightly more performant than type. But this difference is practically negligible in most cases (i. e. if the defined type is not very complex or deeply nested), specially in the later versions. After version 4.0, there is no measurable performance difference in real-world scenarios—stated publicly by the TypeScript Team.
- Interface has closer mapping with objects by definition.
In case of common use cases, like defining an object, or writing a contract for a class, using interface seems more intuitive from object oriented perspective. But we have no way to avoid type in some cases also i. e. while working with union types or so.
The official TypeScript docs suggests to "Use interfaces until you need to use features from type aliases". This pretty much infers to “Use interfaces to define the shape of objects. Use type aliases for more advanced type composition.” For your convenience, I can provide a decision tree—
Use interface when:
- You want declaration merging.
- You want cleaner IntelliSense names.
- You want the error messages to make more sense.
Use type when:
- You need unions, intersections, mapped types, template literals.
- You work with utility types (
Partial,Pick, etc.).
But if you are still confused, just go with whichever you are comfortable with. But, it is recommended to be consistent on the choice. Choose the approach that better suits your style and keeps the code more readable and maintainable.
Keep in mind that, these are compile-time only constructs. After being compiled into JavaScript, no trace of type or interface remains. So choosing one or another does not affect runtime perfomance.
References
TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
Playground Examples 1: https://tinyurl.com/4cwazrus
Playground Examples 2: https://tinyurl.com/4hu8ebsy
Top comments (0)