Should use Type or Interface in Javascript Typescript

Should use Type or Interface in Javascript Typescript




Quick Explanation

  1. You should use types by default until you need a specific feature of interfaces, like 'extends'.
  2. Interfaces can't express unions, mapped types, or conditional types. Type aliases can express any type.
  3. Interfaces can use extends, types can't.
  4. When you're working with objects that inherit from each other, use interfaces. extends makes TypeScript's type checker run slightly faster than using &.
  5. Interfaces with the same name in the same scope merge their declarations, leading to unexpected bugs.
  6. Type aliases have an implicit index signature of Record<PropertyKey, unknown>, which comes up occasionally.

Full Explanation

TypeScript offers a first-class primitive for defining objects that extend from other objects - an interface.Interfaces have been present since the very first version of TypeScript. They're inspired by object-oriented programming and allow you to use inheritance to create types:
interface WithId {
id: string;
}
 
interface User extends WithId {
name: string;
}
 
const user: User = {
id: "123",
name: "Karl",
wrongProperty: 123,
Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};

However, they come with a built-in alternative - type aliases, declared using the type keyword. The type keyword can be used to represent any sort of type in TypeScript, not just object types.

Let's say we want to represent a type that is either a string or a number. We can't do that with an interface, but we can with a type:
type StringOrNumber = string | number;
 
const func = (arg: StringOrNumber) => {};
 
func("hello");
func(123);
 
func(true);
Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.
But, of course, type aliases can also be used to express objects. This leads to a lot of debate among TypeScript users. When you're declaring an object type, should you use an interface or a type alias?

Use Interfaces For Object Inheritance

If you're working with objects that inherit from each other, use interfaces. Our example above, using WithId, can be expressed with type aliases, using an intersection type.
type WithId = {
id: string;
};
 
type User = WithId & {
name: string;
};
 
const user: User = {
id: "123",
name: "Karl",
wrongProperty: 123,
Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.Object literal may only specify known properties, and 'wrongProperty' does not exist in type 'User'.
};


This is perfectly fine code, but it's slightly less optimal. The reason is to do with the speed at which TypeScript can check your types.

When you create an interface using extends, TypeScript can cache that interface by its name in an internal registry. This means that future checks against it can be made faster. With an intersection type using &, it can't cache it via the name - it has to compute it nearly every time.

It's a small optimization, but if the interface is used many times, it adds up. This is why the TypeScript performance wiki recommends using interfaces for object inheritance - and so do I.

However, I still don't recommend you use interfaces by default. Why?

Interfaces Can Declaration Merge

Interfaces have another feature which, if you're not prepared for it, can seem very surprising.
When two interfaces with the same name are declared in the same scope, they merge their declarations.
interface User {
name: string;
}
 
interface User {
id: string;
}
 
const user: User = {
Property 'name' is missing in type '{ id: string; }' but required in type 'User'.Property 'name' is missing in type '{ id: string; }' but required in type 'User'.
id: "123",
};
If you were to try this with types, it wouldn't work:
type User = {
Duplicate identifier 'User'.Duplicate identifier 'User'.
name: string;
};
 
type User = {
Duplicate identifier 'User'.Duplicate identifier 'User'.
id: string;
};

This is intended behavior and a necessary language feature. It's used to model JavaScript libraries that modify global objects, like adding methods to string prototypes.

But if you're not prepared for this, it can lead to really confusing bugs.

If you want to avoid this, I recommend you add ESLint to your project and turn on the no-redeclare rule.

Index Signatures in Types vs Interfaces

Another difference between interfaces and types is a subtle one.

Type aliases have an implicit index signature, but interfaces don't. This means that they're assignable to types that have an index signature, but interfaces aren't. This can lead to errors like:

Index signature for type 'string' is missing in type 'x'.
interface KnownAttributes {
x: number;
y: number;
}
 
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
};
 
type RecordType = Record<string, number>;
 
const oi: RecordType = knownAttributes;
Type 'KnownAttributes' is not assignable to type 'RecordType'. Index signature for type 'string' is missing in type 'KnownAttributes'.Type 'KnownAttributes' is not assignable to type 'RecordType'. Index signature for type 'string' is missing in type 'KnownAttributes'.

The reason this errors is that an interface could later be extended. It might have a property added that doesn't match the key of string or the value of number.

You can fix this by adding an explicit index signature to your interface:
interface KnownAttributes {
x: number;
y: number;
[index: string]: unknown; // new!
}
Or simply, changing it to use type instead:
type KnownAttributes = {
x: number;
y: number;
};
 
const knownAttributes: KnownAttributes = {
x: 1,
y: 2,
};
 
type RecordType = Record<string, number>;
 
const oi: RecordType = knownAttributes;


Isn't that strange!

#Default to type, not interface

The TypeScript documentation has a great guide on this. They cover each feature (although not the implicit index signature), but they reach a different conclusion than me.

They recommend you choose based on personal preference, which I agree with. The difference between type and interface is small enough that you'll be able to use either one without many problems.

But the TS team recommends you default to using interface and only use type when you need to.

I'd like to recommend the opposite. The features of declaration merging and implicit index signatures are surprising enough that they should scare you off using interfaces by default.

Interfaces are still my recommendation for object inheritance, but I'd recommend you use type by default. It's a little more flexible and a little less surprising.
Comment