Menu

Интерфейсы в TypeScript: синтаксис, методы и примеры

Интерфейс даёт имя структуре объекта: какие у него свойства и какие типы они хранят. Как объявить интерфейс, необязательные и readonly свойства, методы, сигнатуры индекса, наследование, реализация в классе, слияние объявлений, обобщённые интерфейсы и как задать значения по умолчанию.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Интерфейс даёт имя структуре объекта: свойствам, которые у него должны быть, и типу каждого из них. После объявления имя используется как тип, и компилятор проверяет по нему каждый объект, который вы передаёте, возвращаете или присваиваете.

Последний вызов это ошибка компиляции TS2741. Интерфейсы стираются при компиляции кода: в выводе JavaScript от User не остаётся следа, и во время выполнения структуру никто не проверяет.

Объявление интерфейса

Синтаксис такой: ключевое слово interface, имя (по соглашению в PascalCase) и тело со списком членов. Члены можно разделять точками с запятой, запятыми или просто переводами строк; точки с запятой это распространённый стиль.

interface Product {
  sku: string;               // required property
  price: number;
  tags: string[];            // array property
  dimensions: {              // nested object type
    width: number;
    height: number;
  };
  discount?: number;         // optional property
  readonly createdAt: Date;  // cannot be reassigned
  label(): string;           // method
}

Интерфейс это тип, а не значение. Его нельзя создать через new, у него нет значений по умолчанию, а obj instanceof Product это ошибка TS2693 ('Product' only refers to a type, but is being used as a value here). Чтобы проверить структуру во время выполнения, напишите защитник типа.

Структурная типизация и проверка лишних свойств

TypeScript сравнивает структуры, а не имена. Любой объект с нужными свойствами подходит под интерфейс, независимо от того, объявлялся ли он с ним. Лишние свойства допустимы, с одним исключением: объектный литерал, написанный прямо там, где ожидается интерфейс, проходит проверку лишних свойств, потому что неизвестный ключ в этом месте почти всегда опечатка.

Именно эта ошибка (TS2353) ловит { id: 1, name: "a", emial: "x" } для User: компилятор даже подсказывает Did you mean to write 'email'? (TS2561).

Необязательные и readonly свойства

? после имени делает свойство необязательным: объект может его опустить, а чтение даёт T | undefined. readonly запрещает переприсваивать свойство после создания объекта.

Здесь видны два ограничения. Во-первых, readonly действует только при компиляции: две строки с пометкой @ts-expect-error при нажатии Run всё равно выполняются и успешно, а последние строки меняют apiUrl через ссылку с типом без readonly. Во-вторых, он поверхностный: readonly hosts: string[] запретил бы переприсваивать hosts, но разрешил бы hosts.push(...), поэтому сам массив типизирован как readonly string[]. Он документирует и обеспечивает намерение в типизированном коде, но ничего не замораживает. Служебный тип Readonly<T> делает readonly сразу все свойства существующего интерфейса.

Методы и свойства-функции

Метод можно записать как сигнатуру метода, name(params): ReturnType, или как свойство, хранящее функцию, name: (params) => ReturnType. Вызывающий код использует оба одинаково.

Разница тонкая: при strictFunctionTypes (входит в strict) параметры свойств с типом функции проверяются строго, а сигнатуры методов мягче (бивариантно), поэтому форма свойства ловит чуть больше ошибок. Синтаксис метода короче и встречается чаще; годятся оба.

Интерфейс может также описывать то, что можно вызвать или сконструировать, через сигнатуру вызова или сигнатуру конструктора:

interface Formatter {
  (value: number): string; // call signature: the object is a function
  locale: string;          // and it also has a property
}

interface PointConstructor {
  new (x: number, y: number): { x: number; y: number }; // construct signature
}

Сигнатуры индекса

Когда имена свойств заранее неизвестны, сигнатура индекса описывает их все сразу: [key: string]: T означает «любой строковый ключ, каждый хранит T».

Последние строки показывают подвох: чтение несуществующего ключа типизировано как number, а не number | undefined. Параметр компилятора noUncheckedIndexedAccess добавляет | undefined к каждому такому чтению.

Именованные свойства могут соседствовать с сигнатурой индекса, но должны ей соответствовать. interface Dict { [key: string]: number; name: string } это ошибка TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Расширьте тип индекса ([key: string]: number | string) или вынесите динамическую часть в отдельное свойство. Для простых отображений ключ/значение Record<string, number> говорит то же самое в одну строку.

Расширение интерфейса

extends строит новый интерфейс из одного или нескольких существующих. Потомок содержит все члены родителя плюс свои:

interface Animal {
  name: string;
}

interface Pet extends Animal {
  owner: string;
}

interface Trained {
  commands: string[];
}

interface ServiceDog extends Pet, Trained {
  certifiedUntil: Date;
}

// ServiceDog requires: name, owner, commands, certifiedUntil

Потомок может переобъявить свойство родителя только с совместимым (более узким) типом, например kind: "dog", где у родителя kind: string. Правила и способы расширять псевдонимы типов описаны на странице extends.

Реализация интерфейса в классе

class X implements Shape просит компилятор проверить, что в классе есть всё, чего требует интерфейс. Отсутствующий член это ошибка в объявлении класса:

index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
  Property 'area' is missing in type 'Circle' but required in type 'Shape'.

Когда area() добавлен, несколько классов и даже обычный объект можно использовать как Shape:

implements это только проверка. Он не добавляет в класс членов и не типизирует за вас параметры методов класса: greet(name) {} внутри класса, реализующего greet(name: string): string, всё равно ошибка TS7006, Parameter 'name' implicitly has an 'any' type. Класс может реализовать несколько интерфейсов: class A implements B, C.

Слияние объявлений

Если объявить интерфейс с одним и тем же именем дважды в одной области видимости, два объявления сливаются в одно. Псевдонимы типов так не умеют (второй type с тем же именем даёт ошибку повторяющегося идентификатора).

interface Settings {
  theme: string;
}

interface Settings {
  fontSize: number;
}

// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };

В прикладном коде это редко нужно, а случайное слияние может сбить с толку. Настоящее применение это добавление членов в типы, которые вам не принадлежат: параметры библиотеки или глобальный объект вроде Window. Внутри модуля оберните объявление в declare global:

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

export {};

После этого window.analytics.track("signup") проходит проверку типов во всём проекте. На том же механизме основаны пакеты определений типов; см. файлы объявлений.

Значения по умолчанию для свойств интерфейса

Интерфейс не может хранить значения по умолчанию, потому что описывает типы и стирается во время выполнения. size?: "sm" | "md" = "md" это ошибка TS1246, An interface property cannot have an initializer. Сделайте свойство необязательным и подставляйте значение по умолчанию там, где используется объект:

Значения по умолчанию при деструктуризации надёжнее: они применяются всякий раз, когда значение undefined, включая явное size: undefined. Вариант с распространением копирует это явное undefined поверх значения по умолчанию, а его результат всё равно типизирован так, будто size задан всегда. Если нужна такая гарантия от типов, включите exactOptionalPropertyTypes, и тогда size: undefined станет ошибкой компиляции для необязательного size?: .... Другой вариант это класс с инициализированными полями, если объекту нужно ещё и поведение.

Обобщённые интерфейсы

Интерфейс может принимать параметры типа, и тогда одно объявление работает для многих типов данных:

interface ApiResponse<T> {
  ok: boolean;
  data: T;
  error?: string;
}

interface Page<T> {
  items: T[];
  nextCursor?: string;
}

interface User {
  id: number;
  name: string;
}

const res: ApiResponse<Page<User>> = {
  ok: true,
  data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};

ApiResponse<Page<User>> читается как «ответ, данные которого это страница пользователей». Стандартная библиотека полна таких интерфейсов: Array<T>, Promise<T>, Map<K, V> это обобщённые интерфейсы.

Интерфейс или псевдоним типа

Псевдоним type может описать ту же структуру объекта, и для простых объектных типов они взаимозаменяемы. Только интерфейс умеет сливаться; только псевдоним типа может назвать объединение, кортеж, сопоставленный или условный тип. Практическое правило из справочника TypeScript: используйте interface, пока не понадобится возможность, которая есть только у type. Полное сравнение, включая неожиданную для многих разницу с Record<string, ...>, есть на странице interface или type.

Часто задаваемые вопросы

Что такое интерфейс в TypeScript?

Интерфейс это именованное описание структуры объекта: имена свойств, их типы, какие из них необязательные или readonly, и методы объекта. Компилятор проверяет, что значения, используемые как этот интерфейс, имеют такую структуру. Интерфейсы существуют только при компиляции и не порождают JavaScript.

Как задать значение по умолчанию в интерфейсе TypeScript?

Никак: интерфейс описывает типы, а не значения, поэтому size: "md" = ... это некорректный синтаксис. Сделайте свойство необязательным (size?: "sm" | "md") и применяйте значение по умолчанию там, где используется объект, обычно через значения по умолчанию при деструктуризации параметров функции: function render({ size = "md" }: Options). Распространение объекта с умолчаниями ({ ...DEFAULTS, ...options }) тоже работает, но явное undefined в options перезапишет значение по умолчанию.

Как проверить во время выполнения, что объект реализует интерфейс?

Встроенного способа нет, потому что интерфейсы стираются при компиляции: obj instanceof User это ошибка TS2693 ('User' only refers to a type, but is being used as a value here). Напишите функцию защитника типа, которая проверяет свойства, function isUser(x: unknown): x is User { ... }, или валидируйте данные библиотекой схем.

Может ли интерфейс расширять несколько интерфейсов?

Да. Перечислите их после extends через запятую: interface ServiceDog extends Pet, Trained { ... }. Новый интерфейс содержит все члены каждого родителя плюс свои. Если два родителя объявляют одно и то же свойство с несовместимыми типами, объявление будет ошибкой.

Чем интерфейс отличается от класса в TypeScript?

Класс существует во время выполнения: у него есть конструктор, реализации методов, и new создаёт из него объекты. Интерфейс только описывает структуру для компилятора и стирается из вывода JavaScript. Класс может объявить implements SomeInterface, чтобы компилятор проверил соответствие, а любой обычный объект с подходящей структурой тоже подходит под интерфейс.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ