Интерфейсы и типы
Интерфейсы как структурные контракты, интерфейс против псевдонима типа, структурная типизация и слияние объявлений.
13 вопросов
JuniorТеорияОчень частоЧто такое interface в TypeScript?
Что такое interface в TypeScript?
Interface описывает форму объекта — имена и типы его свойств и методов — как контракт времени компиляции. Он поддерживает необязательные члены (?), readonly-члены, сигнатуры методов и вызова и индексные сигнатуры. Класс может implements интерфейс, а объектные литералы проверяются по нему. Интерфейсы стираются при компиляции, поэтому не добавляют затрат во время выполнения.
Типичные ошибки
- ✗Думать, что interface существует во время выполнения — он полностью стирается после компиляции
- ✗Путать interface с классом — у интерфейса нет тел методов и конструктора
- ✗Забывать, что
?делает член необязательным, аreadonlyзапрещает переприсваивание
Уточняющие вопросы
- →Чем
implementsотличается отextendsпри использовании на классе? - →Что позволяет выразить в интерфейсе индексная сигнатура?
MiddleТеорияОчень частоКогда использовать interface, а когда type?
Когда использовать interface, а когда type?
Оба могут описывать форму объекта и там во многом взаимозаменяемы. Отличия: interface поддерживает слияние объявлений и является конвенцией для контрактов объектов, классов и публичных API; псевдоним типа type способен также выражать объединения, пересечения, кортежи, mapped- и conditional-типы и давать имя примитиву — ничего из этого interface не умеет. Правило: бери interface для расширяемых форм объектов и классов, а type — для объединений и вычисляемых типов.
Типичные ошибки
- ✗Считать их идеальными синонимами — только
typeделает объединения и вычисляемые типы - ✗Думать, что
interfaceвыразит объединение или назовёт примитив — он не может - ✗Полагать, что любое ключевое слово влияет на выполнение — оба стираются при компиляции
Уточняющие вопросы
- →Какой из них класс может
implements, и может ли он реализовать оба? - →Почему псевдоним типа нельзя переоткрыть, как interface?
JuniorТеорияЧастоПочему лишнее свойство ломает присваивание, когда литерал объекта пишут прямо на месте?
Почему лишнее свойство ломает присваивание, когда литерал объекта пишут прямо на месте?
Проверка лишних свойств — правило для свежих литералов объектов: литерал, присвоенный или переданный прямо на месте, отвергается, если у него есть член, не объявленный в целевом типе. Структурная типизация приняла бы его; правило ловит опечатки вроде widht.
Типичные ошибки
- ✗Верить, что структурная типизация вообще запрещает лишние члены
- ✗Ждать этой проверки от значения, пришедшего через переменную
- ✗Читать ошибку как проблему порождённого JavaScript, а не как ловлю опечатки
Уточняющие вопросы
- →Как меняет результат предварительное присваивание литерала переменной?
- →Что делает с этой проверкой индексная сигнатура, добавленная в целевой тип?
JuniorТеорияЧастоПочему литерал объекта, нигде не объявленный как Point, подходит параметру Point?
Почему литерал объекта, нигде не объявленный как Point, подходит параметру Point?
TypeScript сравнивает типы по форме, а не по имени. Значение подходит Point, как только у него есть требуемые члены: ни implements, ни импорта, ни объявленной связи. В номинальном языке вроде Java имя должно совпасть — там код отвергли бы.
Типичные ошибки
- ✗Думать, что класс или объект обязан объявить
implements, чтобы удовлетворить интерфейсу - ✗Ждать, что одинаковые по форме типы из разных модулей окажутся несовместимы
- ✗Считать, что присваиваемость сверяет имена свойств, но не их типы
Уточняющие вопросы
- →Какую дополнительную проверку получает свежий литерал объекта, но не переменная?
- →Как намеренно сделать два одинаковых по форме типа несовместимыми?
MiddleТеорияЧастоЧто такое структурная типизация в TypeScript?
Что такое структурная типизация в TypeScript?
TypeScript использует структурную («утиную») типизацию — совместимость определяется формой объекта, а не его объявленным именем. Объект присваиваем типу, если у него есть нужные члены, даже если он явно не объявляет этот интерфейс, а два несвязанных типа с одинаковыми членами совместимы. Свежие объектные литералы дополнительно проходят проверку на лишние свойства. Это контрастирует с номинальной типизацией в Java или C#, где объявленное имя должно совпадать.
Типичные ошибки
- ✗Считать, что значение должно объявлять интерфейс по имени — важна лишь его форма
- ✗Ждать несовместимости двух одинаковых по форме именованных типов — они совместимы
- ✗Забывать, что проверка лишних свойств применяется только к свежим объектным литералам
Уточняющие вопросы
- →Почему присваивание литерала с лишним свойством иногда даёт ошибку?
- →Как сымитировать номинальную типизацию, когда она действительно нужна?
MiddleТеорияИногдаПочему тот же объект с лишним свойством проходит, если присвоить его через переменную?
Почему тот же объект с лишним свойством проходит, если присвоить его через переменную?
Литерал теряет свежесть, как только его связали с переменной, а проверку лишних свойств получают только свежие литералы. Остаётся структурная типизация: ей важно лишь наличие требуемых членов. Значение присваивается, и опечатка уцелевает.
Типичные ошибки
- ✗Думать, что лишнее свойство срезается из объекта во время выполнения
- ✗Ждать, что проверка лишних свойств последует за значением через переменную
- ✗Заключать, что тип переменной стал
any
Уточняющие вопросы
- →Как
satisfiesна переменной вернул бы эту проверку? - →Почему TypeScript просто не сделает лишние свойства ошибкой везде?
MiddleТеорияИногдаЧто индексная сигнатура навязывает остальным членам интерфейса?
Что индексная сигнатура навязывает остальным членам интерфейса?
Каждое объявленное свойство обязано укладываться в тип значений сигнатуры, поэтому { [k: string]: number; name: string } — ошибка. Ключ обязан быть string, number, symbol или template-literal-типом. Чтение не проверяется: obj.missing имеет тип number.
Типичные ошибки
- ✗Объявлять свойство, чей тип не укладывается в тип значений индексной сигнатуры
- ✗Ждать, что чтение по неизвестному ключу по умолчанию даст тип
T | undefined - ✗Считать, что ключ индексной сигнатуры может быть любого типа
Уточняющие вопросы
- →Как union в типе значений позволяет типизировать объект смешанной формы?
- →Что именно меняет
noUncheckedIndexedAccessв операции чтения?
MiddleТеорияИногдаКогда стоит опереться на слияние интерфейсов вместо обычного extends?
Когда стоит опереться на слияние интерфейсов вместо обычного extends?
Берите extends, когда тип ваш: это явно, а новое имя само говорит, что перед вами. Слияние — для случая, когда переименовать нельзя: добавить член в чужой тип, например Express.Request или Window. Добавку тогда видит каждая существующая ссылка.
Типичные ошибки
- ✗Браться за слияние там, где яснее обычный
extendsи новое имя - ✗Ждать, что объединённое объявление переопределит член несовместимым типом
- ✗Думать, что существующие ссылки продолжают видеть форму до слияния
Уточняющие вопросы
- →Как влить член в тип, живущий внутри внешнего модуля?
- →Что произойдёт, если два объявления дадут одному члену несовместимые типы?
MiddleТеорияИногдаЧем Record<K, V> отличается от индексной сигнатуры { [k: string]: V }?
Чем Record<K, V> отличается от индексной сигнатуры { [k: string]: V }?
Оба описывают объект, значения которого имеют общий тип V, но ограничивают ключи по-разному. Индексная сигнатура { [k: string]: V } допускает любой ключ этого примитивного вида. Record<K, V> — это mapped-тип над известным набором ключей K, который может быть конечным объединением строковых литералов вроде Record<'a' | 'b', V> — тогда должны существовать ровно эти ключи. То есть Record может задать фиксированный набор ключей, а индексная сигнатура остаётся открытой.
Типичные ошибки
- ✗Думать, что
Recordи индексная сигнатура всегда взаимозаменяемы - ✗Считать, что
Record<'a' | 'b', V>оставляет ключи открытыми, как{ [k: string]: V } - ✗Полагать, что
Record— рантайм-контейнер, а не тип на этапе компиляции
Уточняющие вопросы
- →Когда
Recordс конечным объединением поймает ошибку, которую пропустит индексная сигнатура? - →Могут ли индексная сигнатура и явные именованные свойства сосуществовать в одном типе?
SeniorТеорияИногдаЧто такое слияние объявлений интерфейсов в TypeScript?
Что такое слияние объявлений интерфейсов в TypeScript?
Объявление одного и того же имени интерфейса несколько раз сливает объявления в один интерфейс, члены которого объединяются. Это применяют, чтобы дополнить существующие типы — например, добавить свойства к Window или расширить типы библиотеки через module augmentation. Так сливаются только интерфейсы и namespace; повторный псевдоним типа type с тем же именем — ошибка. Члены с одинаковым именем, но несовместимыми типами в разных объявлениях — тоже ошибка.
Типичные ошибки
- ✗Ожидать, что повторное имя интерфейса даст ошибку дублирования идентификатора
- ✗Думать, что повторный псевдоним
typeсливается так же, как interface - ✗Считать, что последнее объявление заменяет ранние члены, а не объединяет их
Уточняющие вопросы
- →Как module augmentation добавляет свойство к стороннему типу?
- →Что будет, если два сливаемых члена объявят одно имя с разными типами?
SeniorТеорияИногдаПочему type-алиас нельзя переоткрыть так, как интерфейс?
Почему type-алиас нельзя переоткрыть так, как интерфейс?
Интерфейс — открытое имя формы, поэтому второе объявление может добавить члены. Алиас — закрытая привязка к уже вычисленному типу (union, условному), и добавлять члены некуда. Второй алиас с тем же именем — дублирующийся идентификатор.
Типичные ошибки
- ✗Ждать, что два алиаса с одним именем пересекутся так же, как сливаются интерфейсы
- ✗Объяснять ограничение выпуском кода, а не тем, что алиас — закрытая привязка
- ✗Считать, что алиас можно дополнить из модуля потребителя
Уточняющие вопросы
- →Что это значит для библиотеки, чьи типы должны быть расширяемы потребителем?
- →Как сымитировать расширяемый алиас, не отказываясь от union?
SeniorТеорияРедкоВ TypeScript нет точного типа объекта — что вместо него даёт проверка лишних свойств?
В TypeScript нет точного типа объекта — что вместо него даёт проверка лишних свойств?
Поверхностное приближение, привязанное к свежести: проверяется литерал, написанный в месте присваивания, и только он. Значение, пришедшее через переменную, возврат или spread, проносит лишние члены насквозь: структурная типизация спрашивает лишь о требуемых членах.
Типичные ошибки
- ✗Читать проверку лишних свойств как настоящую гарантию точного типа
- ✗Ждать, что проверка последует за значением через переменную, возврат или spread
- ✗Верить, что лишние члены вырезаются из выпущенного объекта
Уточняющие вопросы
- →Как приблизить точный тип через mapped-тип и
never? - →Почему по-настоящему точный тип объекта сломал бы структурную присваиваемость в кодовой базе?
SeniorТеорияРедкоКак namespace сливается с классом или функцией того же имени?
Как namespace сливается с классом или функцией того же имени?
Экспортированные члены namespace становятся статическими членами значения: function f() {} плюс namespace f { export const v = 1 } дают f.v. Они занимают разные пространства объявлений, поэтому компилятор их сцепляет, а не сталкивает.
Типичные ошибки
- ✗Ждать, что два объявления столкнутся как дублирующийся идентификатор
- ✗Объявлять namespace раньше функции или класса, к которым он должен прицепиться
- ✗Верить, что члены попадают в прототип, а не на само значение
Уточняющие вопросы
- →Какие объявления с какими сливаются, а какие пары дают ошибку?
- →Как выразить ту же форму средствами ES-модулей?