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

TypeScript отлично выполняет проверку типов во время компиляции. Но это не означает, что код TypeScript обеспечивает проверку или проверку типа или данных во время выполнения. TypeScript компилирует код в JavaScript без проверки во время выполнения, оставляя дверь открытой для проблем с данными во время выполнения.

Написание кода TypeScript с помощью IDE, которая автоматически показывает ошибки типов по мере их написания, гарантирует, что ваш код правильно обрабатывает полученные данные. Теоретически это делает наши приложения более безопасными. Но, это может убаюкать вас в самоуспокоенность. В конце концов, код TypeScript становится JavaScript, и во время выполнения проверка не выполняется, потому что это JavaScript.

Это означает, что ваш производственный код уязвим для всех видов сбоев из-за неверных данных. Это если вы не знаете о защитном программировании и правильно проверяете свои данные. Кто из нас не забывает делать это везде, где это необходимо? У нас могут быть самые лучшие намерения каждый раз проверять достоверность данных на каждом шагу, но давайте будем честными и признаем, что иногда мы забываем это делать.

Лучше всего реализовать автоматическую проверку данных во время выполнения. Проверка данных может быть встроена в определение класса TypeScript таким образом, чтобы она выполнялась каждый раз, когда вы присваиваете данные полю в классе или каждый раз, когда вы вызываете метод класса.

Чтобы увидеть проблему, рассмотрите следующее:

import { promises as fs } from 'fs';  
class jsdata {
     title: string;
     range: number; 
}  
async function readData(fn: string): Promise<jsdata> {
     const txt = await fs.readFile(fn, 'utf-8');
     const d = JSON.parse(txt);
     const ret = new jsdata();
     ret.title = d.title;
     ret.range = d.range;
     return ret; 
}  
readData(process.argv[2]) .then(data => {
     console.log(data); 
}) .catch(err => {
     console.error(err); 
});

Обычно приложение получает данные JSON, анализирует их, сохраняет данные в объекте, а затем отправляет этот объект в другое место. Вот что демонстрирует этот пример.

Давайте запустим его с хорошими данными:

{
     "title": "Fantastic book title",
     "range": 42 
}

Запустите его так:

$ npx ts-node ./json1.ts ./data1.json jsdata 
{ title: 'Fantastic book title', range: 42 }

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

Но что произойдет, если мы дадим нашему фантастическому алгоритму неверные данные?

{
     "range": "Fantastic book title",
     "title": 42 
}

Это те же данные, но какая-то ошибка перепутала метки. Это может случиться, верно?

$ npx ts-node ./json1.ts ./data2.json jsdata 
{ title: 42, range: 'Fantastic book title' }

Упс, ошибок не было, и теперь наш объект данных имеет неправильные типы данных. Поле title объявлено как string, но вместо него указано number, с той же проблемой, что и с range. В реальном производственном приложении такая ошибка может иметь серьезные последствия в реальном мире.

Мы могли бы почесать затылок и задаться вопросом, как это могло произойти, потому что класс TypeScript четко объявил типы данных. Но взгляните на фактически скомпилированный JavaScript:

"use strict"; 
Object.defineProperty(exports, "__esModule", { value: true }); 
const fs_1 = require("fs"); 
class jsdata { } async function readData(fn) {
     const txt = await fs_1.promises.readFile(fn, 'utf-8');
     const d = JSON.parse(txt);
     const ret = new jsdata();
     ret.title = d.title;
     ret.range = d.range;
     return ret; 
} 
readData(process.argv[2])
.then(data => {
     console.log(data); 
})
.catch(err => {
     console.error(err); 
});

Видите ли вы здесь какие-либо типы данных или проверку типов? На самом деле класс jsdata имеет здесь пустое объявление. Этот код содержит пшик в отношении проверки данных во время выполнения. Это код, который выполняется во время выполнения, а не код TypeScript, показанный выше.

Другими словами, если вы пишете приведенный выше код и верите, что находитесь в безопасности, вас убаюкивают в ла-ла-ландии.

Что требуется, так это проверка данных во время выполнения. Команда TypeScript рекомендует использовать охранники типов, которые представляют собой небольшие функции, обеспечивающие правильность типизации данных. Например, после строки JSON.parse будет строка isJSData(d), которая будет проверять наличие обязательных полей.

Другой подход — декораторы проверки данных во время выполнения в пакете runtime-data-validation. Они позволяют выполнять автоматическую проверку типов каждый раз, когда значение присваивается полю или каждый раз, когда вызывается метод.

Переписывание приведенного выше примера с использованием этих декораторов выглядит следующим образом:

import { promises as fs } from 'fs'; 
import {
     IsInt, IsAscii,
     ValidateParams, ValidateAccessor 
} from 'runtime-data-validation'; 
class jsdata {
     #title: string;
     @ValidateAccessor<string>()
     @IsAscii()
     set title(nt: string) { this.#title = nt; }
     get title(): string { return this.#title; }
      #range: number;
     @ValidateAccessor<number>()
     @IsInt()
     set range(nr: number) { this.#range = nr; }
     get range(): number { return this.#range; }  
}  
async function readData(fn: string): Promise<jsdata> {
     const txt = await fs.readFile(fn, 'utf-8');
     const d = JSON.parse(txt);
     const ret = new jsdata();
     ret.title = d.title;
     ret.range = d.range;
     return ret; 
}  
readData(process.argv[2]) .then(data => {
     console.log({ title: data.title, range: data.range }); 
})
.catch(err => {
     console.error(err); 
});

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

При разработке пакета runtime-data-validators было обнаружено, что мы не можем реализовать проверку данных во время выполнения с помощью декораторов, прикрепленных к атрибутам класса. Это можно было реализовать только в функциях доступа. Таким образом, эта реализация основана на том, чтобы сделать поля title и range закрытыми и использовать вместо них функции доступа.

В случае успеха изменений нет:

$ npx ts-node ./lib/json/json2.ts ./lib/json/data1.json 
{ title: 'Fantastic book title', range: 42 }

Код по-прежнему выполняется, как и ожидалось, и дает правильные результаты.

$ npx ts-node ./lib/json/json2.ts ./lib/json/data2.json 
TypeError: Expected a string but received a number
     at assertString (/home/david/Projects/nodejs/runtime-data-validation-typescript/node_modules/validator/lib/util/assertString.js:17:11)
     at Object.isAscii (/home/david/Projects/nodejs/runtime-data-validation-typescript/node_modules/validator/lib/isAscii.js:17:29)
     at /home/david/Projects/nodejs/runtime-data-validation-typescript/lib/decorators/strings.ts:65:31
     at vfunc (/home/david/Projects/nodejs/runtime-data-validation-typescript/lib/index.ts:87:14)
     at jsdata.descriptor.set (/home/david/Projects/nodejs/runtime-data-validation-typescript/lib/index.ts:247:21)
     at readData (/home/david/Projects/nodejs/typescript-decorators-examples/simple/lib/json/json2.ts:29:14)

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

Краткое содержание

Пакет runtime-data-validation содержит очень длинный список декораторов проверки данных. С ним ваши приложения должны работать более безопасно, поскольку данные будут автоматически проверяться перед тем, как они будут сохранены в полях класса или использованы в методах класса.

Этот пакет — не единственное решение проблемы проверки данных во время выполнения в JavaScript. Существуют такие пакеты, как AJV или JOI, которые помогают с проверкой данных. Кроме того, JSON можно проверить с помощью схемы JSON вместе с пакетом @hyperjump/json-schema.

Проблема с этими подходами заключается в том, что они не предотвращают присвоение неверных данных объектам после проверки объектов. Предположим, вы читаете JSON, как указано выше, затем проверяете его и назначаете данные JSON экземпляру jsdata. Что мешает вашему коду назначить экземпляру неверные данные?

С runtime-data-validation защита с использованием декораторов проверки существует для каждого присвоения защищенному полю и каждого использования защищенного метода.

Чтобы узнать, как были реализованы эти декораторы проверки, см.: Проверка данных во время выполнения в TypeScript с использованием декораторов и метаданных отражения.

об авторе

Дэвид Херрон: Дэвид Херрон — писатель и инженер-программист, занимающийся вопросами разумного использования технологий. Его особенно интересуют экологически чистые энергетические технологии, такие как солнечная энергия, энергия ветра и электромобили. Дэвид почти 30 лет работал в Силиконовой долине над программным обеспечением, начиная от систем электронной почты и заканчивая потоковым видео и языком программирования Java, и опубликовал несколько книг о программировании Node.js и электромобилях.

Первоначально опубликовано на https://techsparx.com.