Различия между псевдонимами типов и интерфейсами в TypeScript 4.6

Примитивные типы

{
type CustomString = string;

const str: CustomString = ‘’;

// ❌

interface CustomStringByInterface = string;
}

Псевдонимы типов могут определять примитивные типы (символ, булевое значение, строку, число, bigint и т. д.), в то время как интерфейсы не могут.

Обратите внимание на то, что псевдоним типа не создает новых типов (отсюда и его название). Интерфейсы, напротив, всегда создают новые типы.

Типы-объединения

{
type Fruit = ‘apple’ | ‘lemon’;

type Vegetable = ‘potato’ | ‘tomato’;

// ‘apple’ | ‘lemon’ | ‘potato’ | ‘tomato’

type Food = Fruit | Vegetable;

const apple: Food = ‘apple’;
}

Типы-объединения также можно определить только с помощью псевдонимов типов.

Типы-кортежи

{
type Animal = [name: string, age: number];

const cat: Animal = [‘’, 1];
}

Типы-кортежи также можно определить только с помощью псевдонимов типов.

Типы объектов/функций

И интерфейс, и тип могут объявлять типы объектов или типы функций. Однако интерфейс может объявлять один и тот же интерфейс несколько раз, и они будут объединены автоматически, в то время как псевдонимы типов не объединяются и должны быть уникальными.

{
// При использовании интерфейсов одинаковые интерфейсы автоматически объединяются.
interface PrintName {
(name: string): void;
}

interface PrintName {
(name: number): void;
}

// ✅

const printName: PrintName = (name: number | string) => {
console.log(‘name: ‘, name);
};
}

{
// При использовании псевдонимов типов они должны быть уникальными, для пересечения можно использовать только `&`.
type PrintName = ((name: string) => void) & ((name: number) => void);

// ✅
const printName: PrintName = (name: number | string) => {
console.log(‘name: ‘, name);
};
}

Другим ключевым моментом является то, что псевдонимы типов используют пересечение, а интерфейсы  —  наследование.

{
interface Parent {
printName: (name: number) => void;
}

// ❌ Интерфейс 'Child' некорректно расширяет интерфейс 'Parent'.
interface Child extends Parent {
printName: (name: string) => void;
}
}
{
type Parent = {
printName: (name: number) => void;
};

type Child = Parent & {
// Здесь два printName будут пересекаться.
// Это эквивалентно`(name: number | string) => void`
printName: (name: string) => void;
};

const test: Child = {
printName: (name: number | string) => {
console.log(‘name: ‘, name);
},
};

test.printName(1);
test.printName(‘1’);
}

Как показано в ошибке выше, когда интерфейс наследуется, подтип не может конфликтовать с супертипом, его можно только расширить:

{
interface Parent {
printName: (name: number) => void;
}

interface Child extends Parent {
// ✅
printName: (name: string | number) => void;
}
}

Интерфейс использует extends для реализации наследования, а псевдоним типа  —  & для реализации пересечения.

В некоторых случаях автоматическое объединение и расширение интерфейсов может пригодиться. Если вы создадите стороннюю библиотеку и откроете публичные API, пользователи смогут расширять ее через механизм интерфейсов!

Поэтому следуйте такому совету: если хотите объявить тип объекта, используйте сначала интерфейс, а при необходимости  —  псевдонимы типов.

Сопоставленные типы объектов

type Vegetable = ‘potato’ | ‘tomato’;
{
type VegetableOption = {
[Property in Vegetable]: boolean;
};

const option: VegetableOption = {
potato: true,

tomato: false,
};

// “potato” | “tomato”
type VegetableAlias = keyof VegetableOption;
}

{
interface VegetableOption {
// ❌ Сопоставленный тип не может объявлять свойства или методы.
[Property in Vegetable]: boolean;
}
}

export {};

Сопоставленные типы объектов могут быть определены только с помощью псевдонимов типов, включая ключевые слова in и keyof.

Неизвестные типы

{
const potato = { name: ‘potato’, weight: 1 };

// тип Vegetable = {
// имя: string;
// вес: number;
// }

type Vegetable = typeof potato;

const tomato: Vegetable = {
name: ‘tomato’,
weight: 2,
};
}

При работе с неизвестными типами можно применять typeof для захвата типа, но в этом случае допустимы только псевдонимы типов, а не интерфейсы.


Подводя итог, можно сказать, что псевдонимы типов охватывают почти все возможности интерфейсов. Однако интерфейсы всегда расширяемы, а псевдонимы типов  —  нет. Поэтому приходится делать выбор между ними в зависимости от конкретного случая.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Lee: Differences Between Type Aliases and Interfaces in TypeScript 4.6

Предыдущая статьяСтруктуры данных: кольцевой (циклический, замкнутый) связный список
Следующая статьяРазработка ПО — системы плагинов