Свои типы: интерфейсы, объединения и обобщения
- Зачем это нужно
- Два способа описать форму
- Объединение и литеральные типы вместо перечисления
- unknown против any: разный момент падения
- Объединение с меткой: результат вместо падения
- Проверка, что все варианты разобраны
- Три уточнения формы
- Обобщения: параметр не для значения, а для типа
- Готовые помощники
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Когда в 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
Разбор данных, пришедших извне, всегда начинают с 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
ошибка: нет связи
После проверки метки компилятор сам сужает тип: в успешной ветке доступны данные, в неуспешной — причина. Обратиться к чужому полю он не даст.
Это тот же приём «результат вместо исключения», о котором пойдёт речь в главе про ошибки, — только записанный в типе.
Проверка, что все варианты разобраны
Когда вариантов три-четыре, легко забыть ветку. Есть приём: в последней, недостижимой ветке присвоить значение типу «ничего»:
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
Без обобщений функция принимает «что угодно» и возвращает «что угодно». Опечатка в имени поля не вызывает ошибки: программа честно группирует по несуществующему полю и создаёт одну группу с ключом «undefined». Дальше по цепочке всё превращается в пустоту, и виноватой окажется какая-нибудь строка через сто строк ниже.
С обобщением и ограничением «только имя поля этого типа» опечатка становится ошибкой сборки — да ещё и с подсказкой правильного имени.
Готовые помощники
В стандартной библиотеке лежат несколько коротких объявлений, которые постоянно встречаются в чужом коде:
| Запись | Что делает |
|---|---|
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и проверку: узел может ответить ошибкой в поле, а не отказом сети; - результат каждой операции описывают объединением с меткой — так эксперт на демонстрации увидит внятное сообщение вместо падения скрипта;
- статусы позиции описывают литеральными типами, а не числами: опечатка станет ошибкой сборки;
- метрики дашборда собирают в один тип и переиспользуют — девять метрик из задания в трёх местах кода.
Отдельно про прямой запуск. Всё, что в этой главе, — чистые типы: они срезаются, и файл запускается напрямую. Это и есть причина, по которой в скриптах предпочитают объединения литералов, а не перечисления.
Что дальше
Формы данных описаны. Следующая глава — про классы: class, поля, инкапсуляция. Там interface из этой главы получит вторую роль — договор, который класс обязуется выполнить, ровно как это было в Solidity.
Углубиться
- Функции-предикаты — своя проверка, после которой компилятор сужает тип.
- Условные типы — тип, который зависит от другого типа; на них написаны готовые помощники.
- Шаблонные литеральные типы — строки, собранные из частей на уровне типов.
- Проверка формы данных библиотекой — как описать форму один раз и получить и тип, и проверку во время работы.
satisfies— проверить, что значение подходит под тип, не теряя точный тип самого значения.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- TypeScript Handbook: Everyday Types — формы, объединения, литералы.
- TypeScript Handbook: Narrowing — сужение и разбор всех вариантов.
- TypeScript Handbook: Generics — параметры типа и ограничения.
- TypeScript: Utility Types — полный список готовых помощников.
- TypeScript Playground — проверить любой пример этой главы без установки.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.