Перейти к основному содержимому
Блокчейн18ГЛАВА 18TypeScript и JavaScript: в чём разницаTS: stringJS

TypeScript и JavaScript: в чём разница

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

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

В прошлых главах сеть уже поднята, нода отвечает, а контракт написан на TypeScript и приехал в докер-образе. Ровно в этот момент почти у каждого возникает вопрос, который стесняются задать вслух: а TypeScript — это вообще что? Отдельный язык? Диалект JavaScript? Почему в проекте лежат и .ts-файлы, и tsconfig.json, и папка dist с почти такими же файлами, только без типов? И почему нода, которая исполняет контракт, про TypeScript ничего не знает?

Ответ на всё это короткий, и с него честнее начать: TypeScript — это JavaScript плюс проверка, которая выполняется до запуска и исчезает после сборки. Никакого второго языка внутри контейнера нет. Есть один язык, который умеет исполнять нода, и есть надстройка, которая до этого исполнения успевает задать тебе неудобные вопросы.

Эта глава — про то, что именно делает эта надстройка, что она успевает поймать, что не поймает никогда, и как всё это выглядит на коде из твоей же работы: контракт, скрипт к ноде, ответ REST-запроса на порт 6862. Ни одной абстрактной задачи про Animal и Dog здесь не будет.

JavaScript: язык, который действительно выполняется​

Начинать надо не с TypeScript, а с того, поверх чего он живёт.

JavaScript — язык, у которого есть исполнитель. Исполнитель называется движком: V8 в Chrome и в Node.js, SpiderMonkey в Firefox, JavaScriptCore в Safari. Движок принимает текст программы и выполняет его. Больше в этой цепочке никого нет: когда контракт на ноде обрабатывает вызов, работает движок, который читает JavaScript.

Два свойства JavaScript, из которых растёт весь смысл TypeScript.

Свойство первое: типы у значений есть, а у переменных — нет. Значение 42 — число, значение '3PJa...' — строка, это правда и в JavaScript. Но переменная не привязана ни к какому типу: сегодня в ней число, через строчку — строка, ещё через строчку — объект. Язык не возражает.

let height = 152340; // число
height = 'сто пятьдесят'; // строка — и это тоже нормально
height = null; // и так тоже

Свойство второе: несовпадение типов почти никогда не останавливает программу. Вместо ошибки JavaScript старается что-нибудь придумать:

const fee = '0.05';
const total = fee * 100; // 5 — строка сама превратилась в число
const label = fee + 100; // '0.05100' — а здесь число превратилось в строку
const missing = undefined + 1; // NaN — «не число», и программа поехала дальше

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

Интересный факт

У ошибок в JavaScript есть неприятная особенность: место возникновения и место проявления почти никогда не совпадают. Строка amount * fee, где fee случайно оказался строкой, отработает молча. NaN поедет дальше, попадёт в тело транзакции, транзакция уедет на ноду, нода её примет — и ошибку ты увидишь в виде странного числа в состоянии контракта через час. TypeScript ценен не тем, что «показывает типы», а тем, что делает место ошибки и место жалобы одним и тем же местом.

Что такое компиляция в JS и почему в ноде нет никакого TS​

Слово «компиляция» в мире JavaScript означает не совсем то, к чему приучают на паре по C++. Там компиляция — перевод исходника в машинный код. Здесь — перевод одного текста программы в другой текст программы, обычно из более удобного человеку в более понятный движку. Такой перевод часто называют транспиляцией, но на практике слова используют вперемешку.

TypeScript-компилятор tsc делает ровно две разные вещи за один проход, и их важно не путать:

  1. Проверяет типы. Читает твои аннотации, сверяет их со всем остальным кодом и выдаёт список несоответствий.
  2. Стирает типы. Убирает из текста всё, чего в JavaScript не существует: аннотации, интерфейсы, псевдонимы типов, дженерики — и сохраняет получившийся JavaScript в файл.

Второй пункт называется type erasure — стирание типов. Он и объясняет, почему на ноде «нет никакого TypeScript».

// contract.ts — то, что пишешь ты
interface Transfer {
recipient: string;
amount: number;
}

export function makeTransfer(t: Transfer): string {
return `${t.recipient}:${t.amount}`;
}
// dist/contract.js — то, что получилось после tsc и что выполняет нода
export function makeTransfer(t) {
return `${t.recipient}:${t.amount}`;
}

Интерфейс Transfer исчез целиком. Не «превратился в проверку», не «остался где-то в метаданных» — просто исчез, потому что в JavaScript такой конструкции нет. Аннотация : Transfer тоже исчезла. Осталась ровно та функция, которую ты бы написал на JavaScript руками.

ТИПЫ ИСЧЕЗАЮТ ПРИ КОМПИЛЯЦИИcontract.tsconst h: string = tx.id;interface NodeInfotype Height = numberтипы видит только tsctscпроверка типовсовпадают ли типыво всех местах кодаошибка илитишинаemitcontract.jsconst h = tx.id;ни одной аннотациине осталосьпишешь ты — TypeScriptаннотации, интерфейсы, дженерикивыполняет Node — JavaScriptTypeScript в рантайме отсутствуетнода исполняет только JS: проверка типов заканчивается там же, где сборка
Слева то, что пишешь ты, справа то, что исполняет нода. Между ними — компилятор, который сначала проверяет типы, а потом вычёркивает их из текста: в рантайме от TypeScript не остаётся ничего.

Отсюда три следствия, которые стоит запомнить дословно.

Следствие первое: типы не существуют во время выполнения. Нельзя спросить у объекта, соответствует ли он интерфейсу Transfer, — интерфейса в рантайме нет. Проверять форму данных приходится обычным JavaScript-кодом: typeof, Array.isArray, наличие поля в объекте.

Следствие второе: TypeScript не делает программу быстрее и не делает её медленнее. На выходе тот же самый JavaScript. Нет ни ускорения от «строгой типизации», ни накладных расходов на проверки — проверки уже закончились, когда сборка завершилась.

Следствие третье: ошибка типов не мешает получить JavaScript. По умолчанию tsc ругается, но файлы всё равно пишет. Именно поэтому в проекте с «красными» типами сборка может выглядеть успешной, а на ноду уехать заведомо кривой код.

Важно

Node.js с версии 22 умеет запускать .ts-файлы напрямую — но делает он это ровно тем же способом, что и tsc: стирает типы и исполняет получившийся JavaScript, ничего не проверяя. Это удобно для запуска скрипта одной командой и абсолютно бесполезно как проверка: файл с грубейшей ошибкой типов запустится молча. Проверку по-прежнему делает отдельный запуск tsc --noEmit — руками, в скрипте npm run typecheck или в CI.

Практический вывод для проекта с контрактом: в репозитории всегда есть две разные команды, и путать их нельзя. tsc --noEmit — «проверь и ничего не собирай». tsc или npm run build — «собери JavaScript, который поедет в докер-образ». Первая ловит ошибки, вторая делает артефакт.

Отвечено вопросов: 0 из 4
Что происходит с интерфейсом TypeScript после компиляции?
Почему нельзя во время выполнения спросить у объекта, соответствует ли он интерфейсу Transfer?
Node.js 22 умеет запускать .ts-файл напрямую. Что это означает для проверки типов?
Влияет ли TypeScript на скорость работы контракта на ноде?
Отвечено верно: 0 из 4

Примитивы и вывод типов​

Первый и самый частый вид аннотации — тип примитивного значения. Их всего несколько, и они соответствуют тому, что и так есть в JavaScript.

ТипЧто означаетПример из работы с нодой
stringстрокаадрес, идентификатор транзакции, base58-строка
numberчисло, любое: и целое, и дробноевысота блока, комиссия, timestamp
booleanправда или ложьфлаг «контракт подтверждён»
bigintцелое произвольной длинысуммы в минимальных единицах, где number уже опасен
null и undefined«значения нет» — два разных способаполе отсутствует в ответе ноды

Аннотация пишется после имени через двоеточие:

const nodeUrl: string = 'http://localhost:6862';
const height: number = 152340;
const isMining: boolean = true;

Но в реальном коде так почти не пишут — и это не небрежность, а норма. TypeScript выводит тип сам, если может:

const nodeUrl = 'http://localhost:6862'; // TypeScript и так знает: string
let height = 152340; // и здесь: number

height = 'ещё немного';
// ^ Type 'string' is not assignable to type 'number'.

Вывод типов (type inference) — причина, по которой TypeScript-код почти не отличается на вид от JavaScript. Аннотации ставят там, где вывести неоткуда: аргументы функций и границы с внешним миром — ответы сети, чтение файлов, входные данные контракта.

Нажми на часть выражения

Отдельно про const и let. В TypeScript у них разное поведение при выводе:

const status = 'succeeded'; // тип — литерал 'succeeded', уже ничего другого туда не положить
let state = 'succeeded'; // тип — string, можно присвоить любую другую строку

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

Union: «одно из перечисленного»​

Дальше начинается то, ради чего TypeScript и берут. Union-тип (объединение) описывается вертикальной чертой и означает «значение — одно из этих».

Самый живой пример из работы с нодой: статус транзакции. У узла Waves Enterprise транзакция может быть в одном из трёх состояний, и они известны заранее:

type TxStatus = 'unknown' | 'unconfirmed' | 'confirmed';

function describe(status: TxStatus): string {
if (status === 'unknown') return 'нода про транзакцию не слышала';
if (status === 'unconfirmed') return 'лежит в пуле, ждёт блока';
return 'в блоке';
}

describe('confirmed'); // ок
describe('succeded'); // ошибка ещё до запуска: такого варианта нет

Опечатка succeded вместо succeeded — классика, которая в JavaScript живёт до продакшена и тихо уводит выполнение в else. В TypeScript она красная в редакторе.

Сужение: как компилятор следит за проверками​

Union полезен не сам по себе, а вместе с сужением (narrowing). Компилятор читает твои проверки и понимает, что внутри ветки тип стал уже:

function heightOrMessage(value: number | string): string {
if (typeof value === 'number') {
return `высота ${value.toFixed(0)}`; // здесь value точно number
}
return value.trim(); // а здесь точно string
}

Никакой магии: компилятор просто умеет читать typeof, ===, in, Array.isArray и instanceof так же, как их читает человек. Обратная сторона — если проверки нет, обращение к методу конкретного типа не пройдёт:

function bad(value: number | string) {
return value.toFixed(0);
// ^ Property 'toFixed' does not exist on type 'string | number'.
}

Это ровно тот случай, когда компилятор ловит настоящую ошибку: строка действительно не умеет toFixed, и в JavaScript этот код упал бы в рантайме на первом же вызове со строкой.

Необязательное поле и «значения нет»​

Ответ ноды часто содержит поля, которых может не быть. Это тоже union — с undefined:

interface TxInfo {
id: string;
height?: number; // вопросительный знак: поле может отсутствовать
}

function show(tx: TxInfo): string {
if (tx.height === undefined) return 'ещё не в блоке';
return `блок ${tx.height}`;
}

Без проверки tx.height имеет тип number | undefined, и арифметика с ним не пройдёт — компилятор напомнит именно про тот случай, который на практике и ломает скрипты: транзакция ещё не попала в блок.

Интерфейсы: описание формы объекта​

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

interface NodeStatus {
blockchainHeight: number;
stateHeight: number;
updatedTimestamp: number;
updatedDate: string;
}

function isSynced(status: NodeStatus): boolean {
return status.blockchainHeight === status.stateHeight;
}

Что даёт это описание на практике, помимо ловли опечаток:

  • Автодополнение. Набрал status. — редактор показывает четыре поля и их типы. Это быстрее, чем открывать документацию, и надёжнее, чем помнить.
  • Документация, которая не устаревает молча. Интерфейс лежит рядом с кодом; если ответ ноды изменился, а интерфейс нет — рассинхрон вылезет в первом же месте использования, а не в проде.
  • Безопасный рефакторинг. Переименовал поле в интерфейсе — компилятор перечислил все места, где оно используется.

interface и type: что выбрать​

TypeScript даёт два способа назвать тип. Для описания объекта они почти взаимозаменяемы:

interface Transfer {
recipient: string;
amount: number;
}

type TransferAlias = {
recipient: string;
amount: number;
};

Практическое правило простое: форма объекта — interface, всё остальное — type. Union, литеральные типы, тип-функция и типы, собранные из других типов, пишутся через type, потому что interface так не умеет. Спорить об этом дольше пятнадцати секунд не стоит: в чужом проекте пиши, как принято вокруг.

Вложенные структуры​

Ответы ноды почти всегда вложенные, и интерфейсы просто складываются друг в друга:

interface ContractCall {
contractId: string;
params: ContractParam[];
}

interface ContractParam {
key: string;
type: 'string' | 'integer' | 'boolean' | 'binary';
value: string;
}

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

Совет

Интерфейс для ответа ноды удобнее всего писать не из головы, а из живого ответа. Дёрни ручку curl -s http://localhost:6862/node/status, скопируй JSON и опиши ровно те поля, которые собираешься использовать. Описывать весь ответ целиком не нужно и вредно: чем меньше полей в интерфейсе, тем меньше он врёт, когда версия ноды поменяется.

Отвечено вопросов: 0 из 4
Что означает тип TxStatus = 'unknown' | 'unconfirmed' | 'confirmed'?
Почему value.toFixed(0) не проходит проверку, если value имеет тип number | string?
Что делает вопросительный знак в поле height?: number интерфейса TxInfo?
Когда для описания типа берут type, а не interface?
Отвечено верно: 0 из 4

Дженерики: тип как параметр​

Дженерики (generics) звучат страшно, а нужны для одной простой вещи: чтобы функция или тип работали с разными типами, не теряя знания о том, с каким именно типом работают сейчас.

Начнём с проблемы. Ты пишешь обёртку над запросами к ноде — одну на весь скрипт:

async function get(path: string): Promise<unknown> {
const res = await fetch(`http://localhost:6862${path}`);
return res.json();
}

Работает, но результат всегда unknown: каждое место вызова обязано разбираться само. Ставить вместо unknown тип NodeStatus нельзя — по этому же пути ходят и другие ручки. Вот здесь и появляется параметр типа:

async function get<T>(path: string): Promise<T> {
const res = await fetch(`http://localhost:6862${path}`);
return (await res.json()) as T;
}

const status = await get<NodeStatus>('/node/status');
// status.blockchainHeight — редактор знает про поле

const height = await get<BlockHeight>('/blocks/height');
// height.height — а здесь знает про другое

T — не конкретный тип, а место, куда подставят тип при вызове. Одна функция, разные результаты, полное знание о результате в каждом месте вызова.

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

  • Promise<NodeStatus> — обещание, что когда-нибудь будет NodeStatus.
  • Array<string> (то же самое, что string[]) — массив строк.
  • Record<string, number> — объект, где ключи строки, а значения числа.
  • Map<string, ContractParam> — словарь параметров контракта по ключу.

Ограничение параметра типа​

Иногда от подставляемого типа что-то требуется. Например, функция логирования результата вызова контракта хочет, чтобы у объекта было поле contractId:

function logCall<T extends { contractId: string }>(call: T): T {
console.log('вызов контракта', call.contractId);
return call;
}

extends здесь не про наследование классов, а про требование: «подставляй любой тип, лишь бы у него было поле contractId типа string». Внутри функции компилятор разрешает обращаться к этому полю, а на выходе возвращает исходный тип, а не урезанный, — вызывающий код ничего не теряет.

Важно

Дженерик в примере с get<T> честно ставит as T — это утверждение типа, а не проверка. Компилятор поверит на слово, а нода про твоё обещание не знает: если ручка вернёт другой JSON, ошибки не будет ни при сборке, ни при вызове get, — она проявится позже и в другом месте. Дженерик здесь удобство для редактора, а не гарантия. Гарантию даёт только проверка данных в коде — про неё раздел про ответ REST-запроса ниже.

any: почему он выключает весь смысл​

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

const data: any = await res.json();

data.hieght + 1; // опечатка в имени поля — молчание
data.foo.bar.baz(); // трёх этих полей не существует — молчание
const n: number = data; // присвоили что угодно числу — молчание

Каждая из этих строк упадёт в рантайме или испортит данные. Ни одна не будет подсвечена.

ANY — ДЫРА В ПРОВЕРКЕ, А НЕ УДОБНЫЙ ТИПответ нодыres.json()форма неизвестнаданные как anyпроверка offкомпилятор молчит на всёвесь код нижеdata.hieght + 1опечатка доживёт до рантаймаany заразен: он передаётся дальше по цепочке присваиванийany — «доверяй мне»ошибку увидишь в продеunknown — «сначала проверь»компилятор требует проверки
any не описывает данные, а отключает проверку — и делает это не в одной строке: тип расползается по всем присваиваниям ниже. unknown решает ту же задачу, но требует проверить данные, прежде чем ими пользоваться.

Хуже того, any заразен. Присвоил any в переменную — она тоже any. Передал в функцию, вернул из функции, положил в поле объекта — проверка перестаёт работать по всей цепочке. Один торопливый any на границе с сетью способен обнулить типизацию половины скрипта, и внешне это никак не видно: код выглядит типизированным.

Чем заменить: unknown​

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

const data: unknown = await res.json();

// data.height; // ошибка: 'data' is of type 'unknown'

if (typeof data === 'object' && data !== null && 'height' in data) {
const height = (data as { height: unknown }).height;
if (typeof height === 'number') {
console.log('высота', height + 1); // здесь уже честно number
}
}

Выглядит длиннее — и это ровно та длина, которую в JavaScript ты не написал бы вовсе, а потом полдня искал бы, откуда взялся NaN. Разница между any и unknown в одной фразе: any говорит компилятору «не мешай», unknown говорит «напомни мне проверить».

Где any всё-таки допустим​

Честности ради: any не запрещён. Он уместен в двух случаях. Первый — временная заглушка при переносе старого JavaScript-кода на TypeScript, когда типы описывают постепенно. Второй — прослойка к библиотеке без типов, у которой ты руками описываешь только нужную часть. В обоих случаях any должен быть локальным и заметным: одна переменная, комментарий рядом, задача в бэклоге. any, который расползся по модулю, — уже не решение, а способ не пользоваться тем, ради чего взяли TypeScript.

Отвечено вопросов: 0 из 3
Что делает тип any со значением?
Чем unknown лучше any на границе с сетью?
Почему один any в начале скрипта опасен для всего файла?
Отвечено верно: 0 из 3

Строгий режим: чем strict отличается от «просто TypeScript»​

Есть TypeScript, который почти ничего не проверяет, и TypeScript, ради которого его берут. Разница — одна строка в tsconfig.json.

Флаг strict включает сразу набор проверок. Три из них меняют жизнь заметнее всего.

strictNullChecks. Без него null и undefined можно положить в любой тип, и обращение к полю у пустого значения компилятор пропустит. С ним — null становится отдельным вариантом, и его надо обработать:

function firstParam(call: ContractCall): string {
const p = call.params[0];
return p.value;
// ^ без проверки на пустой массив это падение в рантайме
}

noImplicitAny. Без него параметр без аннотации молча получает тип any — то есть половина функций проекта оказывается непроверенной, и никто об этом не знает. С ним компилятор требует либо аннотацию, либо явный any, который хотя бы видно глазами.

strictFunctionTypes и родственники — набор более тонких правил про совместимость функций. Их редко разбирают отдельно; достаточно знать, что они входят в strict и что отключать их поодиночке в учебном проекте незачем.

Практическое правило: новый проект начинается со strict: true. Включить строгость в начале — ноль работы. Включить её в проекте на три тысячи строк — день разбора ошибок, часть из которых окажется настоящими багами (и это, кстати, лучший аргумент в пользу флага).

Интересный факт

Когда в шаблоне проекта пишут "strict": true, это разворачивается примерно в восемь отдельных флагов, включая strictNullChecks и noImplicitAny. Отдельные флаги существуют ради постепенного перевода старых проектов: команда включает по одному и чинит ошибки порциями. В новом проекте перечислять их поштучно смысла нет — одна строка делает то же самое и не устареет, когда в очередной версии TypeScript в набор добавят ещё один флаг.

Типизация ответа REST-запроса к ноде​

Теперь соберём всё вместе на самой частой задаче трека: скрипт спрашивает у ноды высоту блокчейна по HTTP и что-то с ней делает.

Начинаем с JavaScript-версии, которую пишут все:

const res = await fetch('http://localhost:6862/blocks/height');
const data = await res.json();
console.log(data.height + 1);

Здесь три невидимых допущения: что ответ вообще пришёл, что это JSON, и что в нём есть поле height с числом. Ни одно не проверено.

ОТВЕТ НОДЫ · ОТ JSON К ТИПИЗИРОВАННОМУ ОБЪЕКТУнодаlocalhost:6862GET /blocks/heightотдаёт обычный JSONграница типовawait res.json()здесь TS ничего не знаето содержимом ответаBlockHeightheight: numberдальше по коду —подсказки и проверкипроверка перед довериемtypeof data.heightравно 'number'интерфейс — обещание о форме ответа; проверка в коде — то, что делает обещание правдой
Ответ ноды приходит как обычный JSON, и в этой точке TypeScript про его содержимое не знает ничего. Интерфейс — обещание о форме, проверка в коде — то, что делает это обещание правдой.

Версия на TypeScript, шаг за шагом.

interface BlockHeight {
height: number;
}

async function fetchHeight(nodeUrl: string): Promise<number> {
const res = await fetch(`${nodeUrl}/blocks/height`);

if (!res.ok) {
throw new Error(`нода ответила ${res.status}`);
}

const data: unknown = await res.json();

if (typeof data !== 'object' || data === null || !('height' in data)) {
throw new Error('в ответе нет поля height');
}

const height = (data as BlockHeight).height;

if (typeof height !== 'number') {
throw new Error('height не число');
}

return height;
}

Что здесь сделано и зачем каждая часть.

  • Promise<number> в сигнатуре — обещание вызывающему коду: получишь число, а не «что-то». Всё, что не число, до вызывающего кода не доедет, потому что превратится в исключение.
  • Проверка res.ok — та самая, которую забывают всегда. fetch не считает ответ 404 ошибкой: промис успешно разрешается, а res.json() дальше падает на разборе HTML-страницы с ошибкой. Типы этого не поймают — это не ошибка типов.
  • data: unknown — честная граница. Компилятор не даст обратиться к data.height, пока не проверишь, и это именно то поведение, которое нужно на входе из сети.
  • Проверка формы — четыре условия подряд: объект, не null, поле есть, поле число. Скучно, но это и есть перевод обещания в факт.
  • as BlockHeight — утверждение типа. После проверок оно оправдано; без проверок это было бы просто вранье компилятору.
Важно

as не преобразует данные и ничего не проверяет — это фраза «поверь мне». Она компилируется в пустоту, как и все остальные типы. Строчка const status = (await res.json()) as NodeStatus; без единой проверки выглядит типизированной, а по надёжности не отличается от any. Отличать нужно два разных действия: описать ожидаемую форму (интерфейс) и убедиться, что данные ей соответствуют (код с typeof). Первое без второго — документация, а не защита.

Отдельно про то, где границ типизации особенно много в блокчейн-скриптах. Их три, и все они выглядят одинаково безобидно:

  1. Ответы REST API ноды — разобраны выше.
  2. Чтение конфигов и переменных окружения. process.env.NODE_URL имеет тип string | undefined, потому что переменной может не быть. Строгий режим заставит это обработать — и правильно сделает: половина «у меня не запускается» именно про незаданную переменную.
  3. Данные, приходящие в контракт из транзакции. Параметры вызова приезжают строками с указанием типа, и превращение строки в число — ручная операция, которую типы не сделают за тебя.
Отвечено вопросов: 0 из 3
Что на самом деле делает выражение (await res.json()) as NodeStatus?
Почему проверку res.ok нельзя заменить типами?
Какой тип у process.env.NODE_URL в строгом режиме и почему?
Отвечено верно: 0 из 3

Что ловит TypeScript и что не ловит никто​

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

ГРАНИЦА ОТВЕТСТВЕННОСТИ ПРОВЕРКИ ТИПОВловит tsc до запускаопечатка в имени поля: tx.snderчисло туда, где ждут строкузабытый await у Promiseнеобработанный вариант unionобращение к полю, которого нетцена ошибки — красная строка в редакторене ловит никтонода вернула не то, что обещаланеверная формула комиссииперепутаны местами два адресасеть отвалилась на серединеконтракт развёрнут не в ту сетьцена ошибки — транзакция в блокчейнетипы проверяют форму данных, а не смысл действийзелёная сборка означает «согласовано», а не «правильно»
Слева — ошибки, которые компилятор находит до запуска, пока цена ошибки равна красной строке в редакторе. Справа — ошибки, о которых он не знает ничего: их цена измеряется уже транзакциями в сети.

Что TypeScript ловит до запуска:

  • Опечатки в именах полей и методов — tx.snder вместо tx.sender.
  • Несовпадение типов при вызове: строка туда, где ждут число.
  • Обращение к полю, которого в типе нет.
  • Забытый await: попытка сложить Promise<number> с числом — ошибка.
  • Необработанный вариант union — если писать функции так, чтобы компилятор мог это заметить.
  • Возможный null и undefined — при включённом strictNullChecks.
  • Последствия рефакторинга: переименовал поле — увидел все места, где о нём знали.

Чего TypeScript не ловит никогда:

  • Неверные данные из сети. Нода вернула не то, что обещал интерфейс, — компилятор об этом не узнает, он читал твой код, а не трафик.
  • Логические ошибки. Формула комиссии умножает вместо деления; типы совпали идеально, результат неверный.
  • Перепутанные значения одного типа. Адрес отправителя и адрес получателя — обе строки. Поменял местами — тишина. Это, пожалуй, самый неприятный класс ошибок в блокчейн-коде.
  • Всё, что происходит в рантайме. Сеть отвалилась, диск полон, нода не поднялась, кончился таймаут.
  • Ошибки конфигурации. Скрипт отработал безупречно и отправил транзакцию не в ту сеть.

Из этого следует правило, которое стоит держать в голове всю дорогу: зелёная сборка означает «код согласован сам с собой», а не «код правильный». Типы убирают целый класс глупых ошибок и освобождают внимание для настоящих. Заменить проверку данных, тесты и здравый смысл они не могут.

Совет

Против перепутанных адресов есть приём, который стоит одной строки и хорошо смотрится в блокчейн-коде: сделать разные типы из одинаковых строк. Если к строке-адресу приписать невидимую метку — отдельный тип для отправителя и отдельный для получателя, — компилятор перестанет пускать одно на место другого, хотя в рантайме это по-прежнему обычные строки. Приём называется брендированными типами; в учебном проекте он избыточен, но знать о нём полезно — он показывает, что «одинаковый тип» и «одинаковый смысл» это разные вещи.

tsconfig.json: четыре настройки, которые реально включают​

tsconfig.json в корне проекта делает две вещи сразу: говорит компилятору, как собирать, и самим своим присутствием отмечает корень проекта. В нём десятки опций, но в учебном и чемпионатном проекте настраивают примерно четыре.

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}

target — в какую версию JavaScript компилировать. Чем ниже значение, тем более старым движкам подойдёт результат и тем больше современных конструкций компилятор перепишет вручную. Для кода, который исполняется на известной версии Node внутри контейнера, занижать target незачем: ES2022 — разумная точка.

module — как оформлять импорты в выходном файле: старый require (CommonJS) или современный import (ES-модули). Самая частая причина сообщения «Cannot use import statement outside a module» — рассинхрон между этой опцией и полем "type" в package.json. Значение NodeNext разбирается с этим само, опираясь на package.json.

strict — разобран в отдельном разделе. Одна строка, ради которой всё и затевалось.

outDir и rootDir — куда класть результат и где лежат исходники. Пара, которая делает проект понятным: src — то, что пишет человек, dist — то, что уезжает в докер-образ. И, кстати, dist — первый кандидат в .gitignore: это результат сборки, он воспроизводится из исходников.

Две оставшиеся строки — из категории «стоит в каждом шаблоне». esModuleInterop чинит импорт старых CommonJS-библиотек через import. skipLibCheck отключает проверку типов внутри .d.ts-файлов зависимостей: чужие ошибки типов не должны ломать твою сборку, а времени эта проверка отнимает много.

Поле include за пределами compilerOptions отвечает на вопрос, какие файлы вообще компилировать. Без него компилятор возьмёт всё, до чего дотянется, включая случайный .ts-файл в папке с черновиками.

Совет

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

Как читать чужой .d.ts​

Рано или поздно редактор по нажатию «перейти к определению» откроет файл с расширением .d.ts — и там будет код без единого тела функции. Это нормально: .d.ts — файл объявлений (declaration file), в котором есть только типы и нет реализации.

Зачем он нужен: библиотека распространяется как JavaScript, а типы в JavaScript не живут. Файл объявлений едет рядом и рассказывает компилятору, что у этой библиотеки есть и какой формы.

Так выглядит фрагмент объявлений в духе тех, что лежат в SDK для работы с контрактами:

export declare class ContractClient {
constructor(config: ClientConfig);
call(contractId: string, params: ContractParam[]): Promise<CallResult>;
getState(contractId: string): Promise<Record<string, string>>;
}

export interface ClientConfig {
nodeUrl: string;
apiKey?: string;
timeout?: number;
}

export type CallResult = { txId: string; height?: number };

Как это читать, если видишь такое впервые:

  • declare — «это существует где-то в JavaScript, тела здесь не будет». Ключевое слово, по которому файл объявлений и узнаётся.
  • Смотри на сигнатуры, а не на реализацию. call(contractId: string, params: ContractParam[]): Promise<CallResult> отвечает сразу на три вопроса: что передать, в каком порядке и что вернётся.
  • Вопросительный знак — необязательный параметр или поле. В ClientConfig обязателен только nodeUrl; apiKey и timeout можно не передавать.
  • Promise<...> в возврате означает await. Если этого не сделать, дальше по коду поедет объект промиса, и компилятор об этом скажет.
  • Иди по ссылкам вглубь. Незнакомый ContractParam — «перейти к определению» ещё раз; через два-три прыжка упрёшься в примитивы.

Практическая ценность привычки: .d.ts — это документация, которая гарантированно совпадает с кодом. README библиотеки может отстать от реальности на три версии, а файл объявлений едет из той же сборки, что и сам код. Когда документация и .d.ts расходятся, прав .d.ts.

Если библиотека своих типов не привозит, редактор скажет «Could not find a declaration file». Обычно это чинится установкой отдельного пакета с типами из общего репозитория DefinitelyTyped — они называются по шаблону @types/имя-библиотеки. Если и его нет, остаётся описать нужный кусок самому — маленьким .d.ts в своём проекте, ровно на те функции, которыми пользуешься.

Когда честно хватает обычного JavaScript​

Хорошая глава про TypeScript обязана содержать раздел про то, когда его не надо брать. Иначе получается не знание, а вера.

Обычный JavaScript уместен, когда:

  • Скрипт на тридцать строк и один запуск. Дёрнуть ручку ноды, посмотреть ответ, забыть. Настройка сборки займёт больше времени, чем сам скрипт.
  • Никто, кроме тебя, этот код не откроет, и открывать его через месяц ты не собираешься.
  • Ты пробуешь идею. На этапе «а сработает ли вообще» типы мешают: форма данных меняется каждые пять минут, и переписывать интерфейсы приходится чаще, чем логику.
  • Среда не даёт собирать. Однострочник в консоли браузера, поле для скрипта в чужом инструменте, node -e в терминале.

TypeScript начинает окупаться там, где появляется хотя бы одно из трёх: чужие данные, чужие люди, время. Ответ ноды — чужие данные. Командный проект — чужие люди. Код, к которому вернёшься через месяц, — время: через месяц ты сам себе чужой человек, и подсказки редактора будут отвечать на вопросы, ответы на которые уже выветрились.

Есть и промежуточный вариант, о котором мало кто знает: типы можно получить, не переходя на TypeScript. Аннотация типов в JSDoc-комментариях внутри .js-файла понимается тем же компилятором, и проверку можно включить одной строкой:

// @ts-check

/**
* @param {string} nodeUrl
* @returns {Promise<number>}
*/
async function fetchHeight(nodeUrl) {
const res = await fetch(`${nodeUrl}/blocks/height`);
const data = await res.json();
return data.height;
}

Файл остаётся обычным JavaScript и запускается без всякой сборки, но редактор проверяет типы и подсказывает. Разумный компромисс для маленьких утилит и для старого проекта, который переводить целиком некогда.

Ну и то, что стоит сказать прямо: для контрактов на треке выбор уже сделан за тебя. SDK привозит типы, шаблоны проектов идут с tsconfig.json, примеры написаны на TypeScript. Спорить с этим на чемпионате — тратить время на переписывание вместо решения задачи.

Практика: набери руками​

Ниже два пула строк вперемешку: слева то, что одинаково выглядит и в JavaScript, и в TypeScript, справа — то, что существует только в TypeScript и исчезнет после сборки. Смысл упражнения не только в скорости: пока набираешь, обрати внимание, какие строки уцелеют в dist, а какие компилятор вычеркнет.

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

const res = await fetch('http://localhost:6862/blocks/height');
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: C — средний левой руки

Шпаргалка: что где живёт​

ВопросJavaScriptTypeScript
Кто исполняетдвижок: V8 в Node.js и Chromeникто: сначала компиляция в JavaScript
Когда видна ошибка типовво время выполнения, если вообщедо запуска, в редакторе и при сборке
Есть ли типы в рантаймеу значений да, у переменных нетнет вовсе: аннотации стёрты
Что даёт редакторудогадки по кодуточное автодополнение и переименование
Цена входанольtsconfig.json и шаг сборки
Проверка формы данных из сетипишется рукамипишется руками — типы её не заменяют
Команда проверкинетtsc --noEmit
Что уезжает в докер-образтвой кодсодержимое dist после tsc

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

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

  • Красные подчёркивания в редакторе — не украшение, а сэкономленные минуты. Каждая пойманная опечатка в имени поля — это не потерянный прогон скрипта против ноды, который занимает минуту-другую вместе с ожиданием блока.
  • Сборка должна проходить. Проект, который не собирается, оценивается как несделанный, даже если «там одна ошибка типов и она неважная». Отдельная команда tsc --noEmit перед сдачей стоит пятнадцати секунд.
  • any в сдаваемом коде читается однозначно. Эксперт видит any на границе с сетью и понимает, что автор либо не разобрался, либо торопился. Заменить на unknown с проверкой — две минуты и другое впечатление.
  • Типы ответов ноды пишутся один раз и работают всю сессию. Пятнадцать минут в начале на интерфейсы к трём ручкам API возвращаются каждым последующим обращением: автодополнение вместо перечитывания документации.
  • Не путай проверку типов с проверкой работы. Зелёная сборка ничего не говорит о том, что транзакция ушла в нужную сеть с нужными параметрами. Это проверяется запуском против ноды, и на это время закладывают отдельно.
Отвечено вопросов: 0 из 3
Скрипт перепутал местами адрес отправителя и адрес получателя. Оба — строки. Поймает ли это TypeScript?
Что означает успешная сборка проекта на TypeScript?
Когда обычный JavaScript без TypeScript — разумный выбор?
Отвечено верно: 0 из 3

Что должен уметь​

  • Объяснять одной фразой, чем TypeScript отличается от JavaScript, и почему на ноде исполняется только JavaScript.
  • Рассказывать, что делает компилятор за один проход: проверяет типы и стирает их.
  • Отличать tsc --noEmit от сборки и понимать, зачем в проекте есть обе команды.
  • Ставить аннотации там, где они нужны: параметры функций и границы с внешним миром, — и не ставить там, где тип выводится.
  • Описывать union-типом набор допустимых значений и сужать его проверками typeof и сравнением.
  • Писать интерфейс для ответа ноды по живому JSON и объяснять, почему описывать весь ответ не надо.
  • Читать дженерик в сигнатуре: Promise<T>, Array<string>, Record<string, number>.
  • Объяснять, почему any отключает проверку и почему unknown — правильная замена на границе с сетью.
  • Понимать, что делает strict, и включать его в новом проекте с первого дня.
  • Типизировать ответ REST-запроса к ноде: интерфейс плюс проверка формы в коде.
  • Перечислять, что типы ловят до запуска и что не ловят никогда.
  • Читать чужой .d.ts и находить в нём сигнатуру нужной функции.
  • Называть случаи, когда обычного JavaScript достаточно.
Углубиться

Структурная типизация. TypeScript сравнивает типы по форме, а не по имени. Объект, у которого есть все нужные поля, подойдёт под интерфейс, даже если про этот интерфейс он ничего не знает и объявлен в другом файле. Это отличается от Java и C#, где важно явное объявление «класс реализует интерфейс», и объясняет, почему интерфейсы к ответам ноды пишут свободно: достаточно описать нужные поля, лишние никому не мешают.

Типы-функции над типами. Кроме описания форм, TypeScript умеет вычислять типы из других типов: Partial<T> делает все поля необязательными, Pick<T, K> оставляет только перечисленные, Omit<T, K> — наоборот. Для скриптов к ноде это удобно, когда из одного большого интерфейса ответа нужно собрать маленький тип для внутренней функции.

Расхождение между .d.ts и реальностью. Файл объявлений пишет человек, и он может ошибиться или отстать от версии библиотеки. Если код ведёт себя не так, как обещают типы, прав всегда рантайм: типы стёрты, а движок исполняет то, что действительно написано в JavaScript.

Валидация вместо ручных проверок. Проверять форму ответа руками через typeof в большом проекте утомительно. Для этого существуют библиотеки схем, которые описывают форму один раз и дают и проверку в рантайме, и тип для компилятора. В учебном проекте они избыточны, но знать про существование класса инструментов полезно: ручная проверка не единственный путь.

Sound и unsound. Система типов TypeScript намеренно не является строго безопасной: в ней есть лазейки (any, as, индексация массива за границей), через которые в рантайм пролезает то, что типы запретили. Это осознанный компромисс ради совместимости с существующим JavaScript — и ещё одна причина не считать зелёную сборку доказательством правильности.

Дальше по теме:

  • TypeScript for JavaScript Programmers — официальное введение ровно для того, кто уже знает JavaScript
  • TypeScript Playground — редактор в браузере, который рядом показывает получившийся JavaScript: лучший способ увидеть стирание типов своими глазами

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

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

  • TypeScript for JavaScript Programmers — короткое официальное введение для тех, кто уже пишет на JavaScript.

  • Everyday Types — примитивы, массивы, объекты, union и псевдонимы типов: девяносто процентов повседневных аннотаций.

  • The Basics — как устроена проверка типов и что именно происходит при компиляции.

  • Narrowing — сужение union через typeof, in, Array.isArray и сравнения.

  • Generics — дженерики от простого примера до ограничений через extends.

  • Declaration Files: Introduction — что такое .d.ts, откуда он берётся и как его читать.

  • TSConfig Reference — все опции tsconfig.json с примерами и значениями по умолчанию.

  • strict — описание флага — что именно включается одной строкой.

  • TypeScript Playground — редактор в браузере, показывающий рядом получившийся JavaScript.

  • Node.js: запуск TypeScript — официальное описание того, как Node стирает типы и почему это не проверка.

  • Node.js: введение в TypeScript — обзор способов запускать TypeScript-код в Node.

  • MDN: JSON.parse — что на самом деле возвращает разбор JSON и почему типов там нет.

  • MDN: fetch — первоисточник про res.ok и про то, что ответ 404 не считается ошибкой.

  • MDN: типы данных JavaScript — какие типы существуют в самом языке, поверх которого работает TypeScript.

  • Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.

Видео по теме

ВидеоЗачем учить TypeScriptHTML Academy
ВидеоTypeScript vs JavaScript: как лучше писать типы, чтобы не создать проблемTimur Shemsedinov
ВидеоTypeScript. Полный курсwebDev
ВидеоTypeScript фундаментальный курс от А до Я: вся теория + практикаUlbi TV
ВидеоJavaScript — полный курс для начинающихBogdan Stashchuk

Комментарии

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