- Cтруктурная типизация
- Псевдонимы типов
- Совместимость
- Отличия
- Что использовать для React Props и State
- Заключение
Псевдонимы 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.