Главная Категории Контакты Поиск

Types vs. Interfaces

Различия между Types и Interfaces.

TypeScript·20.11.2019·читать 5 мин 🤓·Автор: Alexey Myzgin

Псевдонимы types и interfaces часто используются взаимозаменяемо в TypeScript, решение об использовании одного или другого сводится к удобочитаемости/краткости. Они имеют много общего функционального сходства: как и в случае interfaces, types могут расширять другие types (с помощью оператора пересечения &), также могут быть реализованы или расширены классом. Но, они также отличаются по важности: классы не могут быть реализованы, а интерфейсы не могут расширять объединение types.

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

type Pet = IDog | ICat;

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

type Pet = IDog | ICat;

interface IAnimal {
  age: number;
  eat(): void;
  speak(): string;
}

function feedAnimal(animal: IAnimal) {
  animal.eat();
}

class Animal implements IAnimal {
  age = 0;

  eat() {
    console.log("nom..nom..");
  }

  speak() {
    return "roar";
  }
}

interface IDog {}
interface ICat {}

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

Cтруктурная типизация

Псевдонимы типов и интерфейсы также очень похожи. Например, у нас есть interface IAnimal и type AnimalTypeAlias, который присваивает форму объекта имени AnimalTypeAlias.

interface IAnimal {
  age: number;
  eat(): void;
  speak(): string;
}

type AnimalTypeAlias = {
  age: number;
  eat(): void;
  speak(): string;
};

let animalInterface: IAnimal;
let animalTypeAlias: AnimalTypeAlias;

animalInterface = animalTypeAlias;

Эти два типа IAnimal и AnimalTypeAlias в основном эквивалентны. Если мы присвоим переменной let animalInterface тип interface IAnimal, а let animalTypeAlias тип type AnimalTypeAlias, а затем назначим один другому, то TypeScript не будет “ругаться”.

Так как TypeScript использует структурную типизацию. Пока у этих двух типов одинаковая структура (animalInterface и animalTypeAlias), на самом деле не имеет значения, что это разные типы. Если мы изменим любое свойство - TypeScript начнёт “ругаться”.

Псевдонимы типов

Псевдонимы типов, согласно имени, могут действовать как псевдонимы для более сложных типов, таких как функция или массив.

type Eat = (food: string) => void;
type AnimalList = string[];

Мы также можем создать эквивалент этим types, используя interfaces. Эти два interface теперь функционально эквивалентны.

interface IEat {
  (food: string): void;
}

interface IAnimalList {
  [index: number]: string;
}

Хоть интерфейс IAnimalList и пропускает все методы массива, которые есть у AnimalList, он всё равно работает почти как массив, так как он говорит, что все его ключи будут числами, а значения, на которые они указывают, будут строками. Хотя это пример того, как могут быть похожи interfaces и types, это также пример того, в каких ситуациях более лаконично использовать types.

Через types, мы можем указать (выразить) объединение других типов с помощью типов пересечений &. Например, у нас есть тип Dog, который имеет тип IPet, и ICanine:

type Dog = IPet & ICanine;

interface IPet {
  pose(): void;
}
interface ICanine {
  bark: boolean;
}

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

type Dog = IPet & ICanine;

const dog: Dog;
dog.bark; // свойство ICanine
dog.pose(); // свойство IPet

interface IPet {
  pose(): void;
}
interface ICanine {
  bark: boolean;
}

Также, мы можем сделать это с помощью interface. Хотя в этом случае нам нужно будет создать совершенно новый interface для выражения этого слияния.

type Dog = IPet & ICanine;

interface IDog extends IPet, ICanine {

}

Новый interface должен унаследовать как IPet, так и ICanine. Если мы создадим новую переменную интерфейса IDog - у нас будет доступ к свойствам обеих типов. Однако, разница здесь лишь лексическая.

Совместимость

Нет разницы между псевдонимами типов и интерфейсами, когда речь идет об их взаимозаменяемости.

type Pet = {
  pose(): void;
};

interface ICanine {
  bark: boolean;
}

interface IDog extends ICanine, Pet {

}

interface может расширять как interface, так и type. Type может быть пересечением как interface, так и другого type. Даже class может реализовывать implements как interface, так и type.

interface IDog extends ICanine, Pet {}

type Dog = ICanine & Pet;

class HouseDog implements ICanine, Pet {}

Отличия

Одним из наиболее существенных функциональных отличий между type и interface является то, что, с type у нас может быть объединение нескольких других type - в данном случае это type PetType, который может быть либо IDog, либо ICat, то с interface, данная концепция не представляется возможной. Если мы попытаемся расширить в IPet тип объединения PetType, то увидим, что TypeScript “ругается” - An interface can only extend an object type or intersection of object types with statically known members.

type PetType = IDog | ICat;

interface IPet extends PetType {} // error

interface IDog {}
interface ICat {}

interface - это конкретный контракт и мы не можем иметь одно или другое. Он должен быть заблокирован при объявлении. Тоже самое касается class, реализующего один из этих типов объединения PetType. TypeScript будет “ругаться”, ведь class - это образец того, как создавать экземпляры объектов.

type PetType = IDog | ICat;

interface IPet extends PetType {} // error

class Pet implements PetType {} // error

interface IDog {}
interface ICat {}

Ещё одно функциональное различие между interface и type заключается в том, что если у нас два interface с одинаковыми именами, при использовании они будут объединены вместе. Мы получим доступ к свойствам a и b.

interface Foo {
  a: string;
}

interface Foo {
  b: string;
}

let foo: Foo;

foo.a
foo.b

Это то, что types не поддерживают. Если мы сделаем тоже самое с types, то получим ошибку - Duplicate identifier 'Foo'. `.

type Foo = {
  a: string;
}

type Foo = {
  b: string;
}

let foo: Foo;

Теперь давай рассмотрим пример, импортируя jQuery.

import $ from "jquery";

$.fn.extend({
  customFunction: function() {
    // ...
  }
});

$("test").customFunction();

Мы собираемся расширить jquery, добавив к нему новую функцию. Если попытаемся использовать эту функцию, то получим ошибку, что Property 'customFunction' does not exist on type 'JQuery<HTMLElement>'.

Это потому, что его не существует в интерфейсе jQuery по умолчанию, так как мы только что его добавили.

Для того чтобы расширить JQuery локально, нам нужно создать файл _имя_.d.ts (typing.d.ts), который находится в той же папке, и указать, что наш новый метод является частью интерфейса JQuery.

interface JQuery {
  customFunction(): JQuery;
}

Теперь, если вернемся к файлу, в котором у нас была ошибка, то мы её уже не обнаружим. Это происходит из-за поведения, когда TypeScript объединяет объявление interface с тем же именем. Однако, если бы автор библиотеки не пометил JQuery как interface в своем файле, мы бы не смогли сделать что-то подобное.

Что использовать для React Props и State

По большому счету, можно использовать то, что ты хочешь (type alias / interface), только будь последовательным. Лично я рекомендую использовать псевдонимы типов type aliases:

  • меньше писать type Props = {};
  • синтаксис непротиворечив (ты не смешиваешь интерфейсы с псевдонимами типов для возможных пересечений типов);
// ПЛОХО
interface Props extends OwnProps, InjectedProps, StoreProps {}
type OwnProps = {...}
type StoreProps = {...}

// ХОРОШО
type Props = OwnProps & InjectedProps & StoreProps
type OwnProps = {...}
type StoreProps = {...}
  • type полезны для объединяемых типов (например, type MyType = TypeA | TypeB); тогда как интерфейсы лучше использовать для объявления форм, а затем для их реализации или расширения.

Заключение

interface и type очень похожи; они оба могут ссылаться на type, которые объявляют структуру чего-либо. Они также поддерживают объединение различных других типов, либо с помощью оператора пересечения & для type, либо с помощью ключевых слов extend для interface.

Мы можем использовать их взаимозаменяемо. interface могут расширяться от других interface и type. Type могут быть комбинацией interface и type. Классы могут применять (implements) - type и interface.

Однако, мы не сможем применить type, если он является объединением | одного типа или другого - interface, и class должны иметь форму, заблокированную в момент объявления.

  • используй то, что подходит тебе и твоей команде, просто будь последовательным;
  • всегда используй интерфейс для определения общедоступного API при создании библиотечных или сторонних определений типа;
  • рассмотри возможность использования type для React Component Props и State.

Website, name & logo
Copyright © 2022. Alex Myzgin