Интерфейс даёт имя структуре объекта: свойствам, которые у него должны быть, и типу каждого из них. После объявления имя используется как тип, и компилятор проверяет по нему каждый объект, который вы передаёте, возвращаете или присваиваете.
Последний вызов это ошибка компиляции 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, чтобы компилятор проверил соответствие, а любой обычный объект с подходящей структурой тоже подходит под интерфейс.