Type vs interface
Yes, it's yet another article about type vs interface, when to use each and so on, but I promise this one will be different from every one you've seen. We'll cover the basics (that many people know) all the way to the advanced stuff (that few people know). We'll also go a bit further and travel back in time, to understand that this question didn't always exist, for at least two reasons.
Where it all began
Something that always matters when looking at TypeScript is its history. Many people just use features and assume they've always been there, but even the things that make the most sense and seem the most obvious didn't exist until not long ago. That's exactly the point: the "type" keyword for declaring types only shipped in TypeScript 1.4 (6 versions after the first one).
Interfaces, on the other hand, have always existed. You probably know that interfaces can only define objects (if you don't, no problem, we'll get to it shortly). So how did we define string literals, conditional types and so on? Simple: they didn't exist yet. Creating the type keyword made room for a bunch of other features.
That's the first reason the question didn't exist back then. And the second? Well, the idea was that types would be used to define many different kinds of types, like tuples, unions, functions, arrays and so on, but NOT object literals (like we do with interfaces). In the first implementation of the type keyword, using type for an object literal produced an error message.
type type User = {
name: string;
age: number;
}
User = {
name: stringname: string;
age: numberage: number;
};
// Error: Aliased type cannot be an object type literal.
// Use an interface declaration instead.That would make the choice easy: need to define an object? Use interface. For everything else, use type. The idea was precisely to avoid confusion, but that ended up changing.
After some digging, I couldn't reproduce the error above, so I assume it was removed before 1.4 was officially released.
By the way, here's the pull request that implemented the typekeyword, the pull request that allowed types to define object literals, and the discussion about it.
Great, now we know the history behind all this, but now what? One question remains: which one should we use? The TypeScript team itself documented some differences between type vs interface, but certainly not all of them. Let's finally get into it: I'll explain the differences, starting with the most basic ones and working up to the most advanced.
Interfaces can only define objects, types can define anything
This is one of the main differences. Interfaces exist only to declare objects, and objects can have methods and properties. Types can do the same, but they can also define any other type: arrays, tuples, unions, functions, literal types, mapped types, and even primitives like string, number, boolean, date, etc.
One caveat worth mentioning: interfaces can't extend unions, even a union of objects. After all, the end result would be a union.
type type Admin = {
name: string;
role: "admin";
}
Admin = {
name: stringname: string;
role: "admin"role: 'admin';
};
type type Moderator = {
name: string;
role: "moderator";
}
Moderator = {
name: stringname: string;
role: "moderator"role: 'moderator';
};
type type User = Admin | ModeratorUser = type Admin = {
name: string;
role: "admin";
}
Admin | type Moderator = {
name: string;
role: "moderator";
}
Moderator
interface Entity extends User {}; // An interface can only extend an object type or intersection of
// object types with statically known members.Another thing to watch out for: you may have read on Stack Overflow or in articles that only interfaces and classes can be used as a class's contract, and that using a type would be an error.
type type UsersRepository = {
create: (name: string) => string;
}
UsersRepository = {
create: (name: string) => stringcreate: (name: stringname: string) => string;
};
class class UsersUsers implements type UsersRepository = {
create: (name: string) => string;
}
UsersRepository {
Users.create(name: string): stringcreate(name: stringname: string) {
return `alves says hi, ${name: stringname}`;
};
};
// Error: A class may only implement another
// class or interface.That's only partly true. Up to TypeScript 2.1, using a type really was an error; since then, both type and interface can define contracts and be used with implements.
Hovering a type vs an interface
The first visual difference we can spot is that hovering an interface and hovering a type look different: hovering an interface shows only its name, while hovering a type shows its name and the type it defines.
VS
Creating a type with an interface is like creating a brand new type. For example, if something is of type String (capitalized, we're talking about the constructor), the hover shows only the name String (not least because String is implemented with an interface) and not every method and property String has.
With type, it's as if we were only creating an alias for a type that may or may not be anonymous.
Another small visual difference is how interfaces and types show up in error messages. You can see it in this playground.
Interfaces can be redeclared, types can't
Interfaces can be declared multiple times, and TypeScript will automatically merge those declarations as if they were a single interface. This is called 'declaration merging'.
interface User {
User.name: stringname: string;
};
interface User {
User.age: numberage: number;
};
const const user: Useruser: User = {
User.name: stringname: 'alves',
User.age: numberage: 22
};With type, redeclaring is an error. That's because a type is treated as an alias for a specific type (as mentioned in the previous section) and, once defined, it can't be modified or extended by a new declaration.
type User = { name: stringname: string;
} // Duplicate identifier 'User'.
type User = { age: numberage: number;
} // Duplicate identifier 'User'.
const const user: Useruser: type User = {
name: string;
}
User = {
name: stringname: 'alves',
age: 22 // Object literal may only specify known properties, // and 'age' does not exist in type 'User'.
};Declaration merging may or may not be a problem, depending on your situation. But defining a type again with the same name is usually an accident. More on that soon.
Interfaces can't be used to create derived types
Something I always say is: the fewer types you write by hand, the better. To that end, I constantly use and recommend derived types, which usually come from a value that already exists at runtime, via the typeof operator. This approach is less error-prone, means less code, and makes the code more robust (a topic for a future article 👀).
const const apiConfig: {
readonly endpoints: {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
};
readonly headers: {
readonly "Content-Type": "application/json";
readonly Accept: "application/json";
};
readonly timeout: 5000;
readonly retries: 3;
}
apiConfig = {
endpoints: {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
}
endpoints: {
users: "/api/users"users: "/api/users",
products: "/api/products"products: "/api/products",
orders: "/api/orders"orders: "/api/orders"
},
headers: {
readonly "Content-Type": "application/json";
readonly Accept: "application/json";
}
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
timeout: 5000timeout: 5000,
retries: 3retries: 3
} as type const = {
readonly endpoints: {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
};
readonly headers: {
readonly "Content-Type": "application/json";
readonly Accept: "application/json";
};
readonly timeout: 5000;
readonly retries: 3;
}
const;
type type ApiEndpoints = {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
}
ApiEndpoints = typeof const apiConfig: {
readonly endpoints: {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
};
readonly headers: {
readonly "Content-Type": "application/json";
readonly Accept: "application/json";
};
readonly timeout: 5000;
readonly retries: 3;
}
apiConfig.endpoints: {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
}
endpoints;
type type EndpointKeys = "users" | "products" | "orders"EndpointKeys = keyof type ApiEndpoints = {
readonly users: "/api/users";
readonly products: "/api/products";
readonly orders: "/api/orders";
}
ApiEndpoints;The problem is that another limitation of the interface keyword is that it can't represent derived types, even when that type is an object.
const const apiConfig: {}apiConfig = {
// ...
} as type const = {}const;
interface ApiEndpoints extends (typeof apiConfig.endpoints) {}; // An interface can only extend an identifier/qualified-name with
// optional type arguments.There's an open issue in the TypeScript repo that, while not about the typeof operator exactly as in the example, follows a similar idea. Hopefully expressions like this will be allowed in interfaces someday, but for now they don't work.
And an important, maybe obvious, disclaimer: you can use typeof in an interface's properties just fine, only not in the interface's own definition.
interface ApiEndpoints {
ApiEndpoints.values: anyvalues: typeof apiConfig.endpoints;
};Interfaces behave differently from types when composing objects
We compose objects differently: with type we use the '&' operator, which creates an intersection, while with interface we use the 'extends' keyword.
Despite similar purposes, '&' and 'extends' differ. When composing objects, an intersection creates a new object without checking whether the properties' types are compatible. When two or more objects have a property with the same name, their types get intersected, which often results in never.
type type Admin = {
name: string;
document: string;
}
Admin = {
name: stringname: string;
document: stringdocument: string;
};
type type Moderator = {
age: number;
document: number;
}
Moderator = {
age: numberage: number;
document: numberdocument: number;
};
type type User = Admin & ModeratorUser = type Admin = {
name: string;
document: string;
}
Admin & type Moderator = {
age: number;
document: number;
}
Moderator
const const user: Useruser: type User = Admin & ModeratorUser = {
name: stringname: 'alves',
age: numberage: 22,
document: '1234' // Type 'string' is not assignable to type 'never'. // since string & number is equals to never
};With interfaces, properties with the same name must have compatible types. In most cases that's really useful, but it depends on your situation.
interface Admin {
Admin.name: stringname: string;
Admin.document: stringdocument: string;
};
interface Moderator {
Moderator.document: numberdocument: number;
Moderator.age: numberage: number;
};
interface User extends Admin, Moderator {} // Interface 'User' cannot simultaneously extend types 'Admin'
// and 'Moderator'.
// Named property 'document' of types 'Admin' and 'Moderator'
// are not identical. Types have an implicit index signature
A subtle difference between type and interface is that types have an implicit index signature and interfaces don't. Question: is there an error in the code below?
type type User = {
name: string;
}
User = {
name: stringname: string;
};
const const user: Useruser: type User = {
name: string;
}
User = {
name: stringname: 'alves'
};
const const test: {
[key: string]: string;
}
test: { [key: stringkey: string]: string } = const user: Useruser;If you said 'no', you got it! It makes sense: the 'test' variable must be an object with string keys and string values, and 'user' is assignable to that.
But what if we changed 'User' to be declared with an interface instead of a type?
interface User {
User.name: stringname: string;
}
const const user: Useruser: User = {
User.name: stringname: 'alves'
};
const test: { [key: stringkey: string]: string} = const user: Useruser; // Type 'User' is not assignable to type '{ [key: string]: string; }'.
// Index signature for type 'string' is missing in type 'User'.We'd get an error. Weird, right? Remember I mentioned interfaces can be redeclared? That's what causes it: TypeScript knows this interface could be redeclared, so it won't necessarily only have string keys and values, and therefore no index signature is defined implicitly.
We can fix it with an explicit index signature, but then the interface would also accept properties other than 'name'.
interface User {
User.name: stringname: string;
[key: stringkey: string]: string;
}
const const user: Useruser: User = {
User.name: stringname: 'alves'
};
const const test: {
[key: string]: string;
}
test: { [key: stringkey: string]: string} = const user: Useruser; // no errors
// but now this is ok
const const userTwo: UseruserTwo: User = {
User.name: stringname: 'alves',
other: stringother: 'random',
};Interfaces perform better than types when composing many objects
TypeScript is designed so that interfaces are checked in a more optimized way. For performance, it's better to compose many objects with interfaces than with types, since relationships between interfaces are cached. The TypeScript team itself documents this, and you can read more in the TypeScript wiki.
interface Admin {
// a object with a lot of properties
};
interface Moderator {
// a object with a lot of properties
};
interface Owner {
// a object with a lot of properties
}
interface User extends Admin, Moderator, Owner {}In short, once an intersection has more than 2 objects, the performance difference starts to matter, and it has to happen many times to become a real problem. No need to go refactor your intersections into interfaces (yet), but it's worth keeping in mind when choosing between them.
Interfaces have this
We've reached my favorite part of this article, and the most complex/advanced one: the part nobody tells you when you ask about type vs interface. I'll mention 'Higher Kinded Types', but I won't explain them in depth; that topic deserves an article of its own.
Another important difference, one that can decide the choice between interfaces and types: interfaces can use the 'this' type in the return type of functions/methods and in their properties, while types can't.
type type UsersRepository = {
createUser: () => this;
}
UsersRepository = {
createUser: () => thiscreateUser: () => this} // A 'this' type is available only in a non-static
// member of a class or interface.The most common use case is the fluent api pattern, a way to make code more readable by chaining methods so they read like a sentence or a series of instructions.
interface Counter {
Counter.count: numbercount: number;
Counter.increment(): thisincrement(): this;
Counter.reset(): thisreset(): this;
}
class class MyCounterMyCounter implements Counter {
MyCounter.count: numbercount = 0;
MyCounter.increment(): thisincrement() {
this.MyCounter.count: numbercount++;
return this;
}
MyCounter.reset(): thisreset() {
this.MyCounter.count: numbercount = 0;
return this;
}
}
const const counter: MyCountercounter = new constructor MyCounter(): MyCounterMyCounter();
const counter: MyCountercounter.MyCounter.increment(): MyCounterincrement().MyCounter.increment(): MyCounterincrement().MyCounter.reset(): MyCounterreset();With 'this' we can also create mixins: classes/objects that hold methods for other classes/objects, adding functionality without inheritance.
interface Timestamped {
Timestamped.timestamp: Datetimestamp: Date;
Timestamped.setTimestamp(date: Date): thissetTimestamp(date: Datedate: Date): this;
}
interface Named {
Named.name: stringname: string;
Named.setName(name: string): thissetName(name: stringname: string): this;
}
class class UserUser implements Timestamped, Named {
User.timestamp: Datetimestamp = new var Date: DateConstructor
new () => Date (+4 overloads)
Date();
User.name: stringname = "alves";
User.setTimestamp(date: Date): thissetTimestamp(date: Datedate: Date) {
this.User.timestamp: Datetimestamp = date: Datedate;
return this;
}
User.setName(name: string): thissetName(name: stringname: string) {
this.User.name: stringname = name: stringname;
return this;
}
}
new constructor User(): UserUser().User.setName(name: string): UsersetName("John").User.setTimestamp(date: Date): UsersetTimestamp(new var Date: DateConstructor
new () => Date (+4 overloads)
Date());We can also use 'this' in an interface's properties. Look at the behavior below:
interface User {
User.name: stringname: string
User.fullName: this["name"]fullName: this['name']
}This makes the 'fullName' property a string, just like name.
Interesting but useless, right? We could type 'fullName' as string by hand, or even reference the interface itself. So what's the point of 'this'?
interface User {
User.name: stringname: string
User.fullName: stringfullName: string
}
// or
interface User {
User.name: stringname: string
User.fullName: stringfullName: User['name'] // string
}First, a reminder about the unknown type: intersecting it with any other type always gives you that other type. That is:
type type Str = stringStr = unknown & string // string
type type Nbr = numberNbr = unknown & number // number
type type Bool = booleanBool = unknown & boolean // boolean
type type Any = anyAny = unknown & any // any
type type Func = () => voidFunc = unknown & (() => void) // () => void
type type Union = "a" | "b"Union = unknown & 'a' | 'b' // 'a' | 'b'The real advantage comes from combining 'this' with unknown + extends. 'this' always refers to the last type in the extension chain, and any type intersected with unknown resolves to itself, which lets us build this:
interface User {
User.name: stringname: string;
User.age: unknownage: unknown;
User.values: this["age"]values: this['age']; // unknown
}
interface Admin extends User {
Admin.age: numberage: number;
}
const const admin: Adminadmin: Admin = {
User.name: stringname: 'alves',
Admin.age: numberage: 22,
User.values: numbervalues: 22
}The 'values' property is unknown in the 'User' interface, but in 'Admin' it becomes number, since the 'age' property became number.
And that's the behavior libraries use to build Higher Kinded Types. TypeScript doesn't support Higher Kinded Types (HKTs) directly, as languages like Haskell and Scala do, but this is how we can simulate some of their behavior.
Higher Kinded Types (HKTs) are types that operate on other types. While a regular type can be string or number, and a generic type can be something like Array<T> or Promise<T>, a higher kinded type would be something like F<T> where F is a type of types, that is, the abstraction of the abstraction of the abstraction 😅.
Types can be categorized by order/level:
First-order types: simple types like string, number, boolean, etc.
Parameterized (or generic) types: types that take other types as parameters, like Array<T>, Promise<T>, Map<K, V>, etc.
Higher kinded types (HKTs): 'types of types', or 'type constructors'. They aren't complete types on their own; they need other types to form a complete type.
By analogy:
A regular type is like a value (e.g. 5 or 'hello').
A generic type is like a function that takes values f(x) = x+ 1 .
A higher kinded type is like a function that takes functions g(f) = f(f(x))
Since this is a genuinely advanced and complex topic, and a niche one (few people will ever need it, since it shows up mostly in libraries), and given how long this article already is, I'll stop here and let your curiosity lead. If you want to dig deeper and see real use cases, I recommend reading the code of the effect library and this article by the library's maintainer about HKTs.
It's okay if this last section didn't click. That's on me: I still can't simplify the topic enough to give a simple example that shows why it's useful. Most people don't run into this kind of problem, so maybe don't worry about it too much. As I said, it needs an article of its own, and that'll have to wait for next time.
Type or interface?
With all that said, now what? Should I use type or interface?
It depends 😁. Jokes aside, there are a few approaches you can take:
Follow the TypeScript team's recommendation: use interface until you need a feature only type has.
The opposite: use type until you need a feature only interface has.
And finally: always use interface for objects, and type for everything else.
If you're building a library, I suggest reaching for interface first for objects, because declaration merging lets your users extend the interface. But remember they might not need that; it depends on the API you're building.
Final thoughts
First of all, thank you so much for reading this far. I really hope you learned something new and leave knowing which one to use. Personally, I switch between the 2nd and 3rd options. If you found this article useful, share it with someone. If you have questions, DM me on X or Discord (alvseven). If you spotted something wrong, something to correct or a missing detail, please don't hesitate to reach out.
And to wrap up: I may have lied. You can also define functions with interfaces, but it's not very idiomatic, and most of the time you'll see functions defined with type. If you go back to the start of the article, you'll remember the type keyword didn't always exist, so defining functions with interfaces used to be common, like the example below:
interface CreateUser {
(name: stringname: string): { id: stringid: string; name: stringname: string;}
}
const const createUser: CreateUsercreateUser: CreateUser = (name: stringname) => {
return {
id: stringid: '1',
name: stringname,
}
}