Перейти к основному содержимому
Блокчейн25ГЛАВА 25Свои типы: интерфейсы, объединения и обобщенияResultok: truedataok: falseerrorметка различает вариантыформа данных

Свои типы: интерфейсы, объединения и обобщения

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

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

Здесь тип — только договор с компилятором. Он исчезает при запуске и ничего не стоит. Зато он умеет то, чего структура не умела: описывать выбор («либо результат, либо ошибка»), неизвестность («что придёт из сети, мы не знаем») и обобщение («функция работает с любым типом, но помнит, с каким именно»).

Из этих трёх возможностей и получается главное: ошибку, которая иначе всплыла бы у пользователя, ловит компилятор.

Два способа описать форму​

type Tx = { id: string; amount: number };
interface Tx2 { id: string; amount: number }

Для описания объекта они равнозначны: одинаковые ошибки, оба исчезают при компиляции. Разница появляется дальше: type умеет описывать не только объекты — объединения, литералы, функции; interface умеет то, чего не умеет type, — дополняться в другом файле.

Практическое правило: описываете форму объекта — берите любой; описываете что-то кроме объекта — только type.

Объединение и литеральные типы вместо перечисления​

В Solidity состояние позиции описывали перечислением, и под капотом это было число — ничто не мешало сравнить его с числом или получить панику при выходе за диапазон.

Здесь то же самое делается точнее:

type Status = "active" | "liquidating" | "closed";

let s: Status = "active";
s = "closd"; // ошибка компилятора: такого варианта нет
Нажми на часть выражения

И, в отличие от перечисления, это не требует генерации кода — а значит, работает при прямом запуске файла, о чём была глава про запуск.

unknown против any: разный момент падения​

Два типа-«заглушки», которые новички путают. any говорит компилятору «не смотри сюда». unknown — «сюда смотреть можно только после проверки».

Первая модель разбирает один и тот же ответ обоими способами:

--- ПРОВЕРКА ТИПОВ: any ---
(компилятор молчит, 0 ошибок)
--- ЗАПУСК: any ---
TypeError: Cannot read properties of undefined (reading 'address')

--- ПРОВЕРКА ТИПОВ: unknown ---
unknown-case.ts(5,13): error TS18046: 'tx' is of type 'unknown'.
--- ЗАПУСК: unknown ---
запускать нечего: сборка не прошла, ошибка до запуска не дожила

--- unknown + проверка формы ---
id = 0xabc | amount = 100
ОДИН РАЗБОР ОТВЕТА, ДВА ТИПА — И РАЗНЫЙ МОМЕНТ ПАДЕНИЯany — «не смотри сюда»проверка типов: 0 ошибокзапуск: TypeErrorCannot read properties of undefinedпадение увидит пользовательunknown — «сначала проверь»TS18046: 'tx' is of type 'unknown'до запуска дело не дошлопосле проверки формы:id = 0xabc | amount = 100разбор ответа из сети всегда начинают с unknown: что придёт на самом деле, программа не знаетобещание вида «здесь точно такой тип» ничего не проверяет — оно только затыкает компилятор
Разница не в строгости стиля, а в том, кто увидит ошибку: компилятор или пользователь

Разбор данных, пришедших извне, всегда начинают с unknown. Программа не знает, что на самом деле пришло от узла: тот может ответить ошибкой, старой версией формата или вообще не тем. Объявить ответ «точно таким» — значит обмануть себя.

Важно

Обещание вида «здесь точно такой тип» (as) ничего не проверяет — оно только затыкает компилятор, как знак ! из главы про пустоту. Разница между as и настоящей проверкой ровно такая же: одно — слово, другое — код, который выполнится.

Объединение с меткой: результат вместо падения​

Как описать ответ, который может быть успешным или неуспешным? Соблазн — сделать все поля необязательными:

type Naive = { ok: boolean; data?: Balance; error?: string };

Пишется быстро, «покрывает оба случая» — и не содержит связи между ok и наличием данных. Компилятор об этой связи не знает, поэтому молчит.

Правильно — объединение с полем-меткой:

type Result =
| { ok: true; data: Balance }
| { ok: false; error: string };

Вторая модель ставит оба варианта рядом:

--- наивный тип: проверка типов ---
(компилятор молчит, 0 ошибок)
--- наивный тип: запуск ---
TypeError: Cannot read properties of undefined (reading 'balance')

--- объединение с меткой: проверка типов ---
tagged-case.ts(10,26): error TS2339: Property 'data' does not exist on type 'Result'.
Property 'data' does not exist on type '{ ok: false; error: string; }'.

--- объединение + проверка метки ---
баланс: 700
ошибка: нет связи
ОДНО ПОЛЕ-МЕТКА ПРЕВРАЩАЕТ ПАДЕНИЕ В ОШИБКУ СБОРКИнаивно: всё необязательное{ ok: boolean; data?; error? }компилятор: 0 ошибокзапуск: TypeErrorсвязи между ok и data в типе нетобъединение с меткой{ ok: true; data } | { ok: false; error }TS2339: Property 'data' does not existдо запуска дело не дошлопосле проверки метки — обе ветки работаюткомпилятор сам сужает тип после проверки метки: в успешной ветке есть данные, в другой — причинанаивный вариант пишется быстрее и потому опаснее: он выглядит рабочим ровно до первой ошибки узлаэто тот же приём, что «результат вместо исключения», только записанный в типе
Наивный тип пишется быстрее и потому опаснее: он выглядит рабочим ровно до первой ошибки узла

После проверки метки компилятор сам сужает тип: в успешной ветке доступны данные, в неуспешной — причина. Обратиться к чужому полю он не даст.

Это тот же приём «результат вместо исключения», о котором пойдёт речь в главе про ошибки, — только записанный в типе.

Отвечено вопросов: 0 из 2
Чем unknown отличается от any при разборе ответа из сети?
Почему тип «все поля необязательные» опаснее объединения с меткой?
Отвечено верно: 0 из 2

Проверка, что все варианты разобраны​

Когда вариантов три-четыре, легко забыть ветку. Есть приём: в последней, недостижимой ветке присвоить значение типу «ничего»:

function describe(s: Status): string {
switch (s) {
case "active": return "активна";
case "liquidating": return "под ликвидацией";
case "closed": return "закрыта";
default: {
const _exhaustive: never = s; // сюда попадём, только если добавили вариант
return _exhaustive;
}
}
}

Пока разобраны все варианты, в последнюю ветку попадает «ничего», и компилятор доволен. Стоит добавить в тип четвёртый вариант — и он тут же покажет, что здесь его не обработали.

Три уточнения формы​

? — поля может не быть. Обращение без проверки даёт знакомое «значение может быть пустым».

readonly — после создания не менять. Проверка только до запуска: во время работы поле обычное.

& — пересечение. «И то, и другое сразу»: тип, у которого есть поля обоих.

Обобщения: параметр не для значения, а для типа​

function first<T>(items: T[]): T | undefined {
return items[0];
}
Нажми на часть выражения

Третья модель показывает, что теряется без обобщений, на обычной задаче — группировке списка по полю:

--- без обобщений: проверка типов ---
(компилятор молчит, 0 ошибок)
--- без обобщений: запуск ---
ключи группировки: [ 'undefined' ]
сумма первой подтверждённой: undefined

--- с обобщениями: проверка типов ---
strict-case.ts(17,24): error TS2345: Argument of type '"statuss"'
is not assignable to parameter of type 'keyof Tx'.
strict-case.ts(18,28): error TS2551: Property 'amoun' does not exist on type 'Tx'.
Did you mean 'amount'?

--- с обобщениями: правильный вызов ---
ключи: confirmed, pending
подтверждённых: 2
сумма подтверждённых: 140
ЧТО ТЕРЯЕТСЯ БЕЗ ОБОБЩЕНИЙгруппировка по any[] и строкекомпилятор: 0 ошибокключи группировки: [ 'undefined' ]сумма: undefinedопечатка в имени поля прошла молчата же функция с обобщениемTS2345: "statuss" is not assignableTS2551: Did you mean 'amount'?ключи: confirmed, pending · сумма 140компилятор подсказал верное имя самобобщение — это параметр не для значения, а для типа: чем накормили, то и вернётсяограничение «только ключ этого типа» и превращает опечатку в ошибку сборки
Опечатка в имени поля прошла молча и создала группу с ключом «undefined»; с обобщением компилятор подсказал верное имя

Без обобщений функция принимает «что угодно» и возвращает «что угодно». Опечатка в имени поля не вызывает ошибки: программа честно группирует по несуществующему полю и создаёт одну группу с ключом «undefined». Дальше по цепочке всё превращается в пустоту, и виноватой окажется какая-нибудь строка через сто строк ниже.

С обобщением и ограничением «только имя поля этого типа» опечатка становится ошибкой сборки — да ещё и с подсказкой правильного имени.

Отвечено вопросов: 0 из 2
Что такое параметр типа в обобщении?
Группировка по несуществующему полю в версии без обобщений. Что напечатает программа?
Отвечено верно: 0 из 2

Готовые помощники​

В стандартной библиотеке лежат несколько коротких объявлений, которые постоянно встречаются в чужом коде:

ЗаписьЧто делает
Partial<T>все поля становятся необязательными
Readonly<T>все поля только для чтения
Pick<T, K>взять из типа только названные поля
Omit<T, K>взять всё, кроме названных
Record<K, V>объект с ключами K и значениями V

Это не магия компилятора: их можно открыть глазами в файле стандартной библиотеки — каждое занимает две-три строки и написано теми же средствами, что доступны вам.

Типичные ошибки​

any при разборе ответа. Компилятор молчит, падение — при запуске.

Обещание типа вместо проверки. Ничего не проверяет; ошибка та же, что при !.

Наивный тип «всё необязательное» для результата. Связи между признаком успеха и данными нет.

Обращение к полю объединения без проверки метки. TS2339 с подсказкой, в каком варианте поля нет.

Необязательное поле без проверки. Значение может быть пустым.

Опечатка в имени поля при работе со строками. Без ограничения по именам полей проходит молча.

Обобщение без ограничения. Функция принимает что угодно, и половина смысла теряется.

Присвоение объекта с лишним полем. Компилятор подскажет, на какое имя вы, вероятно, ошиблись.

Тип, описанный ради типа. Если тип нигде не используется дважды — скорее всего, он не нужен.

Слова главы​

  • Псевдоним типа (type) и интерфейс (interface) — два способа описать форму.
  • Объединение (|) — «либо то, либо это»; литеральный тип — одно конкретное значение.
  • Метка варианта — поле, по которому компилятор различает варианты объединения.
  • Сужение — компилятор уточняет тип после проверки.
  • never — «сюда не попадём»; ловит забытую ветку разбора.
  • ? / readonly / & — необязательное поле, только для чтения, пересечение.
  • Обобщение (generic) — параметр типа; ограничение (extends) — «только такие типы».
  • keyof — тип «имя любого поля вот этого типа».
  • any / unknown — «не смотри» и «сначала проверь».
  • Готовые помощники — Partial, Readonly, Pick, Omit, Record.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

type Status = "active" | "liquidating" | "closed";

На чемпионате​

Модуль А. Скрипты развёртывания и дашборд — это сплошная работа с формами данных:

  • ответ узла разбирают через unknown и проверку: узел может ответить ошибкой в поле, а не отказом сети;
  • результат каждой операции описывают объединением с меткой — так эксперт на демонстрации увидит внятное сообщение вместо падения скрипта;
  • статусы позиции описывают литеральными типами, а не числами: опечатка станет ошибкой сборки;
  • метрики дашборда собирают в один тип и переиспользуют — девять метрик из задания в трёх местах кода.

Отдельно про прямой запуск. Всё, что в этой главе, — чистые типы: они срезаются, и файл запускается напрямую. Это и есть причина, по которой в скриптах предпочитают объединения литералов, а не перечисления.

Отвечено вопросов: 0 из 2
Почему в скриптах статусы описывают литеральными типами, а не перечислением?
Скрипт разбирает ответ узла. С какого типа начинают?
Отвечено верно: 0 из 2

Что дальше​

Формы данных описаны. Следующая глава — про классы: class, поля, инкапсуляция. Там interface из этой главы получит вторую роль — договор, который класс обязуется выполнить, ровно как это было в Solidity.

Отвечено вопросов: 0 из 1
Зачем в последней ветке разбора вариантов присваивают значение типу «ничего»?
Отвечено верно: 0 из 1

Углубиться​

  • Функции-предикаты — своя проверка, после которой компилятор сужает тип.
  • Условные типы — тип, который зависит от другого типа; на них написаны готовые помощники.
  • Шаблонные литеральные типы — строки, собранные из частей на уровне типов.
  • Проверка формы данных библиотекой — как описать форму один раз и получить и тип, и проверку во время работы.
  • satisfies — проверить, что значение подходит под тип, не теряя точный тип самого значения.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоTypeScript - Union типы и TypeGuardsPurpleSchool | Anton Larichev
ВидеоДженерики в TypeScript с нуля (Generic, Extends, Параметры по Умолчанию + ПримерВасилий Муравьев
ВидеоРазница между Интерфейсом и Типом в Тайпскрипт 2021. TypeScript Interface vs Typwise.js
ВидеоTypeScript / Урок 9. Union TypeИгорь Антонов — про JavaScript и разработку
ВидеоType Narrowing and Type Widening in TypeScriptTypeScript with Benny Code

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.