Сегодня нас ждет один из фундаментальных уроков, который разделяет «пользователей» TypeScript и тех, кто по-настоящему понимает его систему типов. Мы будем говорить об интерфейсах, мощнейшем инструменте для описания структуры объектов. А также мы подробно разберем, чем же interface отличается от уже знакомого нам type и главное, когда что использовать.
Этот урок важен потому, что оба этих понятия повсеместно встречаются в коде на TypeScript и умение выбирать правильный инструмент для задачи сделает ваш код более выразительным, читаемым и соответствующим общепринятым практикам. Не переживайте, если сейчас что-то кажется сложным, мы разложим все по полочкам с помощью множества примеров и практических задач.
Синтаксис interface
Давайте начнем с основ. Интерфейс в TypeScript это абстрактное описание контракта, которому должен следовать объект. Говоря простыми словами, интерфейс определяет, какие свойства и методы должны быть у объекта, не реализуя их. Это лишь blueprint, чертеж.
Синтаксис объявления интерфейса очень прост. Мы используем ключевое слово interface, за которым следует его имя (обычно с заглавной буквы), а затем в фигурных скобках мы перечисляем все необходимые свойства и методы с их типами.
// Объявляем интерфейс User interface User { id: number; name: string; email: string; age?: number; // Необязательное свойство } // Используем интерфейс для аннотации типа объекта const newUser: User = { id: 1, name: 'Максим', email: 'max@example.com' // age мы можем не указывать, так как оно необязательное };
В этом примере мы создали интерфейс User. Теперь любой объект, который мы захотим пометить типом User, обязан иметь свойства id, name и email соответствующих типов. Свойство age является необязательным, благодаря символу ?.
Интерфейсы могут описывать не только свойства, но и методы. Мы можем описать сигнатуру функции, которая должна быть реализована в объекте.
interface Car { brand: string; model: string; startEngine: () => void; // Метод без аргументов и возвращаемого значения getDetails: (detailed: boolean) => string; // Метод, который принимает boolean и возвращает string } const myCar: Car = { brand: 'Toyota', model: 'Camry', startEngine() { console.log('Двигатель запущен!'); }, getDetails(detailed: boolean) { return detailed ? `${this.brand} ${this.model}, V6 3.5L` : `${this.brand} ${this.model}`; } }; myCar.startEngine(); // "Двигатель запущен!" console.log(myCar.getDetails(true)); // "Toyota Camry, V6 3.5L"
Обратите внимание, как мы реализовали методы startEngine и getDetails внутри объекта myCar. Интерфейс лишь диктует что должен делать объект, но не как он это делает. Реализация остается за самим объектом.
Одной из ключевых особенностей TypeScript является «утиная типизация» (Duck Typing). Если объект имеет все свойства и методы, описанные в интерфейсе, то он считается соответствующим этому интерфейсу, даже если мы явно не указали его тип. Это очень мощная концепция.
interface Point { x: number; y: number; } // Эта функция ожидает объект, соответствующий интерфейсу Point function printPoint(point: Point) { console.log(`Координаты: (${point.x}, ${point.y})`); } // Мы создаем объект без явного указания типа Point const myPoint = { x: 10, y: 20, z: 30 }; // Есть даже лишнее свойство z! // TypeScript не выдаст ошибку! // Объект myPoint имеет x: number и y: number, этого достаточно. printPoint(myPoint); // Координаты: (10, 20)
Как видите, TypeScript проверил, что у объекта myPoint есть всё необходимое для работы в качестве Point и проигнорировал лишнее свойство z. Это позволяет очень гибко работать с существующим JavaScript-кодом.
Сравнение interface и type: сходства
На первый взгляд, interface и type могут показаться взаимозаменяемыми. И в многих случаях это действительно так. Давайте сначала посмотрим на их сходства.
1. Описание объекта. И то и другое может использоваться для описания формы объекта.
// С помощью type type UserType = { id: number; name: string; }; // С помощью interface interface UserInterface { id: number; name: string; } // Использование абсолютно идентично const user1: UserType = { id: 1, name: 'Type' }; const user2: UserInterface = { id: 2, name: 'Interface' };
2. Необязательные свойства и свойства только для чтения. Оба механизма поддерживают модификаторы ? для необязательных свойств и readonly для свойств только для чтения.
type PersonType = { readonly ssn: string; // Нельзя изменить после создания name: string; hobby?: string; // Необязательное }; interface PersonInterface { readonly ssn: string; name: string; hobby?: string; }
3. Описание функций. Оба могут использоваться для описания типа функции.
// Type Alias для функции type SumFunctionType = (a: number, b: number) => number; // Interface для функции interface SumFunctionInterface { (a: number, b: number): number; } // Оба варианта работают одинаково const add: SumFunctionType = (x, y) => x + y; const subtract: SumFunctionInterface = (x, y) => x - y;
Из-за этой схожести часто возникает закономерный вопрос: «Так что же использовать?». Ответ кроется в различиях, которые мы рассмотрим далее. Понимание этих различий это ключ к проффесиональному использованию TypeScript.
Сравнение interface и type: различия
Несмотря на сходства, между interface и type есть фундаментальные различия, которые определяют их применение.
1. Объединения (Unions) и Пересечения (Intersections).
Это самое важное концептуальное различие.
-
Type Aliases могут описывать не только объекты, но и примитивные типы, объединения (union) и кортежи (tuples). Это их суперсила.
// Примитив type ID = number | string; // Объединение (Union) type Status = 'pending' | 'in-progress' | 'completed'; // Кортеж (Tuple) type DataPair = [string, number]; // Intersection (&) - объединение нескольких типов в один type Name = { firstName: string; lastName: string }; type Contact = { phone: string }; type Person = Name & Contact; // Person теперь имеет все свойства из Name и Contact const person: Person = { firstName: 'Иван', lastName: 'Иванов', phone: '+79990000000' };
-
Interfaces предназначены только для описания формы объекта. Вы не можете использовать
interfaceдля создания псевдонима для примитива, union или tuple.// Это НЕВОЗМОЖНО с interface interface Status = 'pending' | 'in-progress'; // Ошибка!
Однако интерфейсы поддерживают наследование (extends), которое является их способом создания пересечений для объектов.
interface Name { firstName: string; lastName: string; } interface Contact { phone: string; } // Интерфейс Person наследует (extends) все свойства от Name и Contact interface Person extends Name, Contact {} const person: Person = { firstName: 'Иван', lastName: 'Иванов', phone: '+79990000000' };
Результат для типа
Personв обоих случаях одинаков, но достигнут разными путями:typeиспользует оператор&(intersection), аinterfaceиспользует ключевое словоextends.
2. Расширение (Extending).
Способы расширения, еще одно ключевое различие.
-
Расширение Interface: Интерфейсы могут наследоваться друг от друга с помощью ключевого слова
extends. Это очень читаемо и понятно, особенно если вы знакомы с ООП.interface Animal { name: string; } // Интерфейс Bear расширяет (наследует от) Animal interface Bear extends Animal { honey: boolean; } const bear: Bear = { name: 'Винни', honey: true };
Интерфейс может наследоваться от нескольких интерфейсов одновременно.
interface Mammal { hasFur: boolean; } interface Bear extends Animal, Mammal { honey: boolean; } const bear: Bear = { name: 'Винни', hasFur: true, honey: true };
-
Расширение Type: Псевдонимы типов (type) расширяются через оператор
&(intersection).type Animal = { name: string; }; type Bear = Animal & { honey: boolean; }; const bear: Bear = { name: 'Винни', honey: true };
3. Декларативное слияние (Declaration Merging).
Это уникальная особенность интерфейсов, которой нет у псевдонимов типов.
Если вы объявите интерфейс с одним и тем же именем несколько раз в одной области видимости, TypeScript автоматически объединит все эти объявления в один интерфейс.
interface Window { title: string; } interface Window { ts: TypeScriptAPI; } // В итоге это будет один интерфейс Window: // { // title: string; // ts: TypeScriptAPI; // } const src = 'const a = "Hello World"'; const window: Window = { title: 'My Window', ts: someAPI }; window.ts.transpileModule(src);
Это поведение крайне полезно при расширении существующих определений типов, например, когда вы хотите добавить кастомные свойства к глобальному объекту window в вашем веб-приложении.
С type такое невозможно. Если вы попытаетесь создать type с уже существующим именем, компилятор выдаст ошибку.
type Window = { // Ошибка: Duplicate identifier 'Window'. title: string; };
Когда что использовать?
Теперь самый практичный вопрос: в какой ситуации выбрать interface, а в какой type?
Используйте interface когда:
-
Вы описываете форму объекта. Это первоначальная и главная цель интерфейсов. Синтаксис
extendsочень直观ен и хорошо читается. -
Вам нужна возможность декларативного слияния. Это важно при написании
.d.tsфайлов определений для библиотек или расширении существующих типов. -
Вы работаете в кодовой базе, которая следует Object-Oriented принципам. Интерфейсы, с их наследованием, идеально вписываются в ООП-стиль.
Используйте type когда:
-
Вам нужно описать не только объект: примитив, union-тип, intersection-тип или кортеж.
type ID = string | number; type Direction = 'up' | 'down' | 'left' | 'right'; type Coord = [number, number];
-
Вам нужно использовать возможности mapped types или более сложные преобразования типов (это тема для более продвинутых уроков).
-
Вам нужно создать тип на основе другого типа с помощью операторов (например,
keyof,typeof).
Общее правило от Максима:
-
Начинайте с
interfaceдля описания форм объектов и публичного API, особенно если ваша кодовая база активно их использует. -
Переключайтесь на
type, когда вам нужны возможности, которые есть только у него: unions, tuples, etc.
Не стоит воспринимать это как жесткое правило. Часто выбор диктуется стандартами вашей команды или конкретного проекта. Самое главное быть последовательным.
Практические примеры и задачи
Давайте закрепим теорию на практике. Вот несколько задач и примеров.
Задача 1: Создание интерфейса
Создайте интерфейс Movie со следующими свойствами:
-
title(строка, обязательное) -
director(строка, обязательное) -
year(число, обязательное) -
rating(число, необязательное) -
genres(массив строк, обязательное)
Напишите функцию printMovie(movie: Movie), которая выводит информацию о фильме в консоль.
Решение:
interface Movie { title: string; director: string; year: number; rating?: number; genres: string[]; } function printMovie(movie: Movie): void { console.log(`Фильм: ${movie.title}`); console.log(`Режиссер: ${movie.director}`); console.log(`Год: ${movie.year}`); if (movie.rating !== undefined) { console.log(`Рейтинг: ${movie.rating}/10`); } console.log(`Жанры: ${movie.genres.join(', ')}`); } const inception: Movie = { title: 'Начало', director: 'Кристофер Нолан', year: 2010, rating: 8.8, genres: ['фантастика', 'боевик', 'триллер'] }; printMovie(inception);
Задача 2: Наследование интерфейсов
Создайте базовый интерфейс Entity с полями id: number и createdAt: Date.
Расширьте его, создав интерфейс User, который добавляет поля email: string и username: string.
Создайте объект типа User.
Решение:
interface Entity { id: number; createdAt: Date; } interface User extends Entity { email: string; username: string; } const currentUser: User = { id: 12345, createdAt: new Date('2023-01-15'), email: 'user@example.com', username: 'cool_user' };
Задача 3: Различие между extends и &
Попробуйте решить одну и ту же задачу двумя способами.
Создайте тип/интерфейс Employee, который объединяет свойства Person (с полями name: string и age: number) и Job (с полями company: string и position: string).
-
Способ 1: Используя
interfaceи наследование (extends). -
Способ 2: Используя
typeи intersection (&).
Решение:
// Способ 1: interface interface Person { name: string; age: number; } interface Job { company: string; position: string; } interface Employee extends Person, Job {} // Способ 2: type type PersonType = { name: string; age: number; }; type JobType = { company: string; position: string; }; type EmployeeType = PersonType & JobType; // Объект будет соответствовать и интерфейсу и типу const employee: Employee = { name: 'Мария', age: 28, company: 'TechCorp', position: 'Разработчик' }; const employee2: EmployeeType = employee; // Это тоже будет работать!
Задача 4: Union type (только для type)
Создайте тип Result, который может находиться в одном из двух состояний:
-
Success: содержит полеdata: string -
Error: содержит полеerrorCode: number
Напишите функцию handleResult(result: Result), которая обрабатывает оба случая.
Решение:
// Эту задачу можно решить ТОЛЬКО с помощью type type Result = | { status: 'success'; data: string } // Вариант 1 | { status: 'error'; errorCode: number }; // Вариант 2 function handleResult(result: Result): void { // Здесь нам помогает контроль потока на основе типа (type narrowing) if (result.status === 'success') { console.log(`Успех! Данные: ${result.data}`); // TypeScript знает, что здесь result имеет тип { status: 'success'; data: string } } else { console.log(`Ошибка ${result.errorCode}`); // TypeScript знает, что здесь result имеет тип { status: 'error'; errorCode: number } } } handleResult({ status: 'success', data: 'Загрузка завершена' }); handleResult({ status: 'error', errorCode: 404 });
В следующем уроке мы перейдем к еще более мощным концепциям, которые строятся на основе интерфейсов, обобщенное программирование (Generics). Это позволит нам писать по-настоящему переиспользуемый и типобезопасный код.
Это был урок 9 из моего полного курса по TypeScript для начинающих. Если вы хотите освоить язык с нуля и структурировать свои знания, вам будет полезно ознакомиться с полной программой курса. Там вас ждут все 40 уроков, от основ до продвинутых тем, упражнения и финальный проект.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


