what and when to use?

My name is Dima. I am a Frontend developer at fuse8. When working with TypeScript, sooner or later you come across the question: what to choose – types or interfaces? In our team, we actively use TypeScript, paying special attention to types. In this article, I would like to share the features of working with types and interfaces that may be useful in your practice.

Main differences between types and interfaces

Types are used to define named data types, including primitives, objects, functions, and arrays. They allow types to be combined or intersected, and support the use of the typeof, keyof keywords when assigning.

Interfaces are used to describe the structure of objects. Interfaces support declarative unification and can be extended by other interfaces or classes.

AND typesAnd interfaces allow you to describe data structures in TypeScript, which helps prevent compile-time errors and make code more predictable.

For primitives and tuples, use types

It is simply not possible to create a string, number, or other primitive type using an interface.

Example with primitives:

type UserId = string;
type ColumnHeight = number;
type isActive = boolean;

In interfaces, primitive types can be used in describing object properties:

interface User {
  id: string;
  age: number;
  isActive: boolean;
}

Example with a tuple:

type Coordinates = [number, number];

You can achieve similar behavior using the interface, but it is not recommended:

 interface Coordinates {
     0: number;
     1: number;
     length: 2; // фиксированная длина
 }

Interfaces with the same names are merged

Interfaces have a feature that types don't: if you have multiple interfaces with the same name, they can unite. This is especially useful when you are working with external libraries or projects where the object structure needs to be extended.

Let's look at an example:

interface User {
  id: number;
}

interface User {
  name: string;
}

const user: User = {
  id: 100,
  name: 'John Doe'
};

In this example there are two interfaces User merge into one that contains both properties: id And name. This allows you to flexibly add new fields to existing structures without touching the original code. If you tried to do the same with types, TypeScript would throw an error – type names must be unique, even if the types were in different files.

The merger does not occur at the level of a single file, but at the level of the entire project. Therefore, it is important to remember, especially if the project is large, that it is possible to accidentally extend an existing interface. This rule also works for pre-defined interfaces, for example, if you need to type a comment using an interface by choosing a name Commentthen we will expand the interface Commentwhich is located in lib.dom.d.ts.

For more immersion, you can check out documentation on interface unification.

Types can be intersected and combined, interfaces can be inherited

The intersection of types is carried out using the operator &:

type User = { id: string; };
type Article = { title: string; };

type UserArticle = User & Article;

Here UserArticle combines properties of both the user and the article.

Similar behavior in interfaces can be achieved using the keyword extends:

interface User {
 id: string;
}

interface Article {
 title: string;
}

interface UserArticle extends User, Article {}

But it's not the same thing, extends is used only for interfaces and implies inheritance, whereas intersection of types using & can be used for both interfaces and any other types.

There is an opinion that interface inheritance works fasterthan type intersections. This is because extension operations require fewer compile-time resources than type intersections. TypeScript Performance Guide It is also recommended to prefer inheritance over interfaces if compilation speed is important.

However, real tests show that the difference is insignificant. For example, testing 10 thousand identical constructions for interfaces and types did not reveal a significant difference in compilation speed. The experiment can be found Here.

Another difference is that if both types are objects, and these objects contain fields with the same names but different types, then extends will give an error, and when used& there will be no error. Let's look at an example:

type User = {
 id: string;
}

type Article = {
 id: number;
}

type UserArticle = User & Article;

IN UserArticle there is no error, but id has a type neverbecause id cannot be both a string and a number. And when using extends we get an error:

Types also support union using the operator |. This is convenient when the type can be one of several options:

type User = {
 id: string;
}

interface Article {
 title: string;
}

type ProductId = string;

type Payload = User | Article | ProductId;

Keep types concise when using Utility Types

The types have more more concise syntax when using Utility Types, than interfaces. For example, to create a type with optional fields, you can use the Partial utility.

Here's what it looks like for types:

type User = {
  id: string;
}

type UserPartial = Partial<User>;

Now let's see how it will look with the interface:

interface User {
  id: string;
}

interface UserPartial extends Partial<User> {}

In the case of an interface, we have to add additional extends constructs and empty curly braces {}, which makes the code less readable. This is not critical, but it can add extra “noise”, especially if utilities such as Partial, Pick, Omit and others are often used.

Interface properties preserve source

Another interesting feature of interfaces is that their properties retain information about where they came from. This can be useful when debugging code.

Example:

interface User {
  id: string;
}

interface Article {
  name: string;
}

interface UserArticle extends User, Article {};

const userArticle: UserArticle = {
  id: 'test',
  name: 'test'
};

If you look at the object userArticlefield id will be associated with User.id: stringand name — with Article.name: string. This can help to better understand where a particular property comes from in complex inheritances.

Now let's rewrite the same example on types:

type User = {
  id: string;
}

type Article = {
  name: string;
}

type UserArticle = User & Article;

const userArticle: UserArticle = {
  id: 'test',
  name: 'test'
};

In the case of types, both fields are debugged id And name will be just strings, and the information about where they came from will be lost.

When to use types and when to use interfaces?

A rule you can take as a basis is to use default types and interfaces when necessary.

The use of interfaces can be considered in libraries that will be installed in projects to allow types to be extended if necessary. Or in projects that use the OOP approach.

Useful links:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *