Рубрики
Без рубрики

Находится ли машинопись на Node.js достаточно хорош для разработчиков Java?

Время от времени вы сталкиваетесь с капризным программистом, который ненавидит Javascript. Они утверждают, что Javascript… С тегами node, java, javascript, typescript.

Время от времени вы сталкиваетесь с капризным программистом, который ненавидит Javascript. Они утверждают, что JavaScript ужасен, потому что в нем отсутствует проверка типов или что-либо строгое. Они будут настаивать на том, что системы корпоративного уровня требуют определенной степени строгости, которую можно найти только в строго типизированных языках (таких как Java, C# или C++).

Спор продолжается. В “маленьком” приложении анонимные объекты со слабо типизированными полями вполне приемлемы. Но, безусловно, система с миллионами строк кода может действительно работать только в том случае, если компилятор и система выполнения помогают программистам находить ошибки. Только язык со строгой типизацией и другими функциями может предотвратить определенные классы ошибок программирования.

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

Может быть. Введите Typescript.

В этой статье я буду оценивать Typescript с точки зрения опытного Java-программиста , который освоил JavaScript, Node.js , и Vue.js и т.д. в большом смысле. Мне любопытно, насколько Typescript может улучшить мои способности к кодированию на JavaScript.

Оснастка и настройка

Набор инструментов Typescript написан на Node.js . Конечно, ваш первый шаг – установить Node.js и нпм. Мы будем использовать Node.js версия 10.x в этой статье (10.12 – последняя версия на момент написания этой статьи) из-за ее поддержки модулей ES6.

Из краткого руководства по запуску Typescript вы узнаете, что typescript устанавливается следующим образом:

$ npm install -g typescript

Пакет Typescript рекомендуется устанавливать глобально (опция -g). Он устанавливает команду tsc, которая является компилятором Typescript. Целью компиляторов является генерация исходного кода JavaScript из файлов Typescript. JavaScript – это то, что будет выполняться, и это то, что вы должны развернуть в браузерах или как Node.js модули.

Теперь вы можете ввести это, чтобы просмотреть инструкции по использованию:

$ tsc — help

Другим очень полезным инструментом является ts-node , вариант команды node, которая непосредственно выполняет исходный код typescript.

Он установлен следующим образом:

$ npm install ts-node -g

После установки доступна команда ts-node.

Следующий этап заключается в настройке Node.js проект для того, чтобы следовать примерам в этой статье. Сначала создайте пустой каталог, затем запустите npm init, чтобы настроить пустой npm/Node.js проект.

В том же каталоге создайте конфигурационный файл typescript, tsconfig.json, который может содержать следующее:

{
 "compilerOptions": {
 "lib": [ "es5", "es6", "es7",
 "es2015", "es2016", "es2017", "es2018", "esnext" ],
 "target": "es6",
 "moduleResolution": "node"
 }
}

Это говорит о компиляции в соответствии со спецификациями ES5/ES6/etc, что и Node.js 10.x орудий труда. Он выводит код, используя спецификацию ES6, опять же, это то, что доступно в Node.js 10.x.

Вы можете найти больше об этом в документации Typescript .

Последнее, что нужно настроить, – это специальная поддержка для Node.js в машинописном виде.

Мы добавим коллекцию DefinitelyTyped Typescript — огромную коллекцию типов для конкретных библиотек или платформ в экосистеме JavaScript.

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

Добавление определяемых определений для Node.js обеспечивает поддержку определенных Node.js особенности. Мы делаем это, чтобы предотвратить конкретную проблему, которая в противном случае возникла бы у нас с объектом process .

Есть разница между тем, что Node.js делает для своих традиционных модулей (на основе спецификации модулей CommonJS) и что он делает для модулей ES6. В традиционном Node.js модули, вводятся несколько объектов, таких как module и process . Эти объекты не являются частью спецификации модуля ES6 и поэтому недоступны в модулях ES6.

Поскольку Typescript использует модули ES6, эти объекты отсутствуют, что не позволяет нам их использовать. В скрипте, который мы запустим позже, нам нужно получить аргументы из командной строки, которая, конечно же, использует объект process .

Решение состоит в том, чтобы установить пакет @types/node. Этот пакет является частью коллекции с окончательной типизацией и предоставляет определения для Node.js . Все, что требуется, – это установить пакет как зависимость от разработки:

$ npm install — save-dev @types/node

Краткий пример

Давайте начнем с варианта руководства по быстрому запуску. Создайте файл, назовите его greeter.ts (обратите внимание на расширение “.ts”), содержащий:

function greeter(person: string) {
 return "Hello, " + person;
}

let user = "Jane User";
// let user = [0, 1, 2];

console.log(greeter(user));

Затем выполните его следующим образом:

$ ts-node greeter.ts
Hello, Jane User

С помощью команды ts-node нам не нужно ничего настраивать, она просто запускает код. Конечно, это не будет работать для производства, для чего мы должны скомпилировать Typescript в JavaScript.

Компиляции выполняются следующим образом:

$ tsc greeter.ts 
$ cat greeter.js 
function greeter(person) {
 return "Hello, " + person;
}
var user = "Jane User";
// let user = [0, 1, 2];
console.log(greeter(user));

Источником Typescript является простой JavaScript, за исключением списка параметров функции greater.

function greeter(person: string) { … }

Вот тут-то нам и начинает помогать Typescript. Параметр/| person объявляется с типом string. В обычном JavaScript у нас нет помощи со стороны компилятора, чтобы избежать проблем с параметром, передаваемым этой функции. Вызывающий может передать что угодно, и в JavaScript это не имеет значения. Но что, если наша функция правильно выполняется только со строкой?

В традиционном JavaScript мы бы вручную проверили тип следующим образом:

if (typeof greeter !== "string") throw new Error("bad type for person");

Написание нашего кода таким образом было бы более надежным, но большинство из нас не утруждает себя этим. В классической книге Элементы стиля программирования , Керниган и Плаугер, авторы настоятельно рекомендуют использовать защитное кодирование. То есть, чтобы проверить параметры функции, прежде чем предполагать, каковы они есть, потому что в противном случае программа может выйти из строя.

Вот тут-то и появляются языки со строгой проверкой типов. Компилятор (и среда выполнения) вмешиваются, чтобы сделать то, что большинство из нас не утруждает себя выполнением.

В примере кода вы увидите два варианта пользовательского объекта, один из которых представляет собой массив. Измените исходный код на этот:

// let user = "Jane User";
let user = [0, 1, 2];

С этим мы столкнулись с проблемой. Пользовательский объект представляет собой массив и не соответствует списку параметров функции:

$ ts-node greeter.ts 
/Volumes/Extra/logrocket/typescript/start/node\_modules/ts-node/src/index.ts:261
 return new TSError(diagnosticText, diagnosticCodes)
 ^
TSError: ⨯ Unable to compile TypeScript:
greeter.ts(8,21): error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

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

С этим мы начинаем видеть, как формируется “победа”. С языком строгой проверки типов это выглядит так, как будто у нас есть компилятор, который должен дважды проверить, что мы не зафиксировали проблему с кодированием.

Интерфейсы Typescript и более крупный пример

Typescript обладает целым рядом интересных функций, аналогичных Java или C # языки. Например, у него есть концепция класса, которая является надмножеством того, что было определено в ES-2015/6, с добавлением типов, конечно. Но при просмотре документации одна особенность, которая выделяется, – это их подход к интерфейсам.

В Java и других языках объекты интерфейса являются ключом к гибкости. Интерфейс – это не полноценный класс. Вместо этого это атрибут, который может быть применен к классам. Например, в Java java.util. Интерфейс списка реализован несколькими конкретными классами, такими как ArrayList, LinkedList, Stack и Vector. Вы можете передать любую из этих реализаций списка любому методу, объявленному для приема списка, и метод не заботится о конкретной реализации.

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

Функция интерфейса Typescript – это утиный ввод, подкрепленный синтаксисом языка Typescript. Один объявляет имя интерфейса интерфейса { .. fields }, а затем имя интерфейса может использоваться в качестве типа в параметрах метода или полях в объектах. Во время компиляции компилятор Typescript, выполняя статический анализ кода, проверяет, соответствуют ли объекты какому-либо интерфейсу, объявленному в каждом параметре или поле.

Чтобы попробовать простой пример, создайте файл и назовите его interface 1.ts, содержащий это:

enum Gender {
 male = "male", female = "female"
}

interface Student {
 id: number;
 name: string;
 entered: number;
 grade: number;
 gender: Gender
};

for (let student of [
 {
 id: 1, name: "John Brown", 
 entered: 1997, grade: 4,
 gender: Gender.male
 },
 /\* {
 id: "1", name: "John Brown", 
 entered: 1997, grade: 4,
 gender: Gender.male
 }, 
 {
 id: 1, name: "John Brown", 
 entered: 1997, grade: 4,
 gender: "male"
 } \*/
]) {
 printStudent(student);
}

function printStudent(student: Student) {
 console.log(`${student.id} ${student.name} entered: ${student.entered} grade: ${student.grade} gender: ${student.gender}`);
}

Что мы сделали, так это определили интерфейс и несколько анонимных объектов. Анонимные объекты не были объявлены для реализации интерфейса student, это просто объекты. Но эти анонимные объекты находятся в цикле, который передает объекты для печати вызовов учащихся. Используя статический анализ кода, компилятор Typescript видит, что каждый объект должен соответствовать интерфейсу student.

Когда Typescript сопоставляет объект с интерфейсом, он проходит по полю через определение интерфейса, сопоставляя поля в предоставленном объекте. Чтобы объект рассматривался как реализующий интерфейс, у него должны быть все соответствующие поля, а типы должны совпадать. Вы можете узнать больше в documentation .

Запустите пример, показанный выше, и вы получите это:

$ ts-node interface1.ts
1 John Brown entered: 1997 grade: 4 gender: male

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

Раскомментируйте эти две записи в массиве, и вместо этого вы получите следующее:

$ ts-node interface1.ts
/Volumes/Extra/logrocket/typescript/start/node\_modules/ts-node/src/index.ts:261
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
interface1.ts(31,18): error TS2345: Argument of type '{ id: string; name: string; entered: number; grade: number; gender: Gender; } | { id: number; name: string; entered: number; grade: number; gender: string; }' is not assignable to parameter of type 'Student'.
Type '{ id: string; name: string; entered: number; grade: number; gender: Gender; }' is not assignable to type 'Student'.
Types of property 'id' are incompatible.
Type 'string' is not assignable to type 'number'.

Опять же, мы успешно обнаружили обычную проблему — передачу неправильно структурированных объектов в функцию. Второй элемент массива — поле идентификатора — использует строку, а не числовое значение, что приводит к ошибке здесь. В третьем элементе массива поле gender использует простую строку, а не Gender.male или Gender.female.

Еще одна победа. Но в следующем разделе мы рассмотрим, как Typescript подводит нас.

Извлечение из внешнего хранилища — проверка типа во время выполнения

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

Решая эту проблему, мы открываем банку с червями. Поскольку Typescript выполняет проверку типов только во время компиляции, это не помогает нам выявлять проблемы во время выполнения. Это недостаток по сравнению с такими языками, как Java или C#, где проверка типов выполняется во время выполнения. По пути мы узнаем достаточно о Typescript, чтобы вынести суждение в заключении.

Мы будем использовать файл YAML для внешнего хранения данных при построении предыдущего примера. Создайте новый файл interface 2.ts, содержащий этот:

import \* as yaml from 'js-yaml';
import { promises as fs } from 'fs';
import \* as util from 'util';

class Registry {
 private \_yaml: string;
 private \_parsed: any;

 async load(fn: string): Promise {
   this.\_yaml = await fs.readFile(fn, 'utf8');
   this.\_parsed = yaml.safeLoad(this.\_yaml);
 }

 get students(): Student[] {
   if (this.\_parsed) {
     let ret: Student[] = [];
     for (let student of this.\_parsed.students) {
       try {
         ret.push({
           id: student.id,
           name: student.name,
           entered: student.entered,
           grade: student.grade,
           gender: student.gender
         });
       } catch (e) {
         console.error(`Could not convert student ${util.inspect(student)} because ${e}`);
       }
    }
    return ret;
  }
 }
}

let registry: Registry = new Registry();

let fn = process.argv[2];
registry.load(fn)
.then(res => { listStudents(); })
.catch(err => { console.error(err); });

async function listStudents(): Promise {
 for (let student of registry.students) {
   printStudent(student);
 }
}

function printStudent(student: Student): void {
 console.log(`${student.id} ${student.name} entered: ${student.entered} grade: ${student.grade} gender: ${student.gender}`);
}

enum Gender {
 male = "male", female = "female"
}

interface Student {
 id: number;
 name: string;
 entered: number;
 grade: number;
 gender: Gender
};

В первую очередь мы добавили класс реестра, который обрабатывает извлечение данных учащихся из файла YAML. На данный момент единственные данные, которые он будет поддерживать, – это массив записей учащихся. Очевидно, что другие элементы данных могут быть сохранены в файле YAML для более полного приложения. Получатель с именем students получит доступ к массиву записей информации о студентах.

Затем создайте файл students.yaml, содержащий эти данные:

students:
 — id: 1
   name: John Brown
   entered: 1977
   grade: 4
   gender: male
 — id: "2"
   name: Juanette Brown
   entered: 1977
   grade: "4"
   gender: female
 — id: 3
   name: Nobody
   entered: yesterday
   grade: lines
   gender: None-of-your-Business

В YAML-else это массив с именем students, содержащий поля, которые соответствуют интерфейсу student. За исключением того, что, как мы увидим, ни одна из данных точно не соответствует интерфейсу учащегося. Третий имеет значения, очевидно, сильно отличающиеся от интерфейса student.

В классе registry у нас есть функция load, которая считывает текст YAML, а затем преобразует его в объект. Данные хранятся в закрытых членах класса.

Определения классов Typescript являются надмножеством объекта класса, введенного в ES6. Одним из дополнений являются закрытые и защищенные ключевые слова, которые создают меру сокрытия информации. Мы можем хранить эти локальные данные в экземпляре объекта и иметь некоторую уверенность в том, что другой код не получит доступ к этим данным.

В середине скрипта вы увидите, что мы создаем экземпляр реестра, а затем вызываем registry.загрузка, за которой следует список учащихся, который проходит и печатает список учащихся.

В реестре.загрузка мы были достаточно откровенны с объявлениями типов. Параметр fn (filename) объявляется строкой, а функция объявляется так, чтобы ничего не возвращать. Поскольку load – это асинхронная функция, Typescript заставляет нас объявлять ее как Promise, поскольку асинхронные функции всегда возвращают обещание. Этот синтаксис означает Обещание , которое аннулируется . Этот синтаксис похож на универсальную функцию других языков (что и является целью).

В Typescript синтаксис для Массива объектов Foo – Foo[]. Следовательно, метод students accessor объявлен для возврата массива объектов student.

Чтобы заполнить массив объектов student, мы создаем простые объекты из данных в файле YAML. Так получилось, что поля в нашем файле YAML соответствуют тому, что определено в интерфейсе student, так что это должно работать нормально (постучать по дереву).

Чтобы привлечь поддержку YAML:

$ npm install js-yaml — save

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

$ ts-node interface2.ts students.yaml 
(node:9798) ExperimentalWarning: The fs.promises API is experimental
1 John Brown entered: 1977 grade: 4 gender: male
2 Juanette Brown entered: 1977 grade: 4 gender: female
3 Nobody entered: yesterday grade: lines gender: None-of-your-Business

Эта первая строка, о fs.promises, является побочным продуктом использования fs Promises API . Не беспокойтесь об этом, так как мы используем его для упрощения кодирования.

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

Неправильный. Проблема в том, что все эти элементы должны были выйти из строя из-за того, что типы данных не соответствовали интерфейсу student. Для второй и третьей записей несколько полей являются строками, тогда как они должны были быть числами, и поэтому не соответствуют типу в интерфейсе учащегося. Поле gender ни в коем случае не содержит перечисления Gender, вместо этого оно всегда содержит строку.

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

$ tsc

С уже показанной конфигурацией это компилирует файлы Typescript в JavaScript с использованием целевого объекта, настроенного в tsconfig.json. Скомпилированный JavaScript – это то, что на самом деле выполняется, поэтому просмотр этого кода полезен при попытке понять, почему ваша программа ведет себя не так, как ожидалось.

В скомпилированном коде, interface2.js , вы увидите, что это функция printStudent:

function printStudent(student) {
  console.log(`${student.id} ${student.name} entered: ${student.entered} grade: ${student.grade} gender: ${student.gender}`);
}

Это чистая простая функция, но видите ли вы какую-либо проверку типов? Нет. Вы также не видите ничего подобного в остальной части скомпилированного кода. Опять же, отличная проверка типов Typescripts происходит только во время компиляции, а не во время выполнения.

Мы были глупы, думая, что сможем прочитать массив и напрямую использовать его в качестве объектов student. Средство получения students должно быть написано с защитой и проверять объекты, которые мы получаем, чтобы убедиться, что они соответствуют объявлению интерфейса student, и сопоставлять их с соответствующим экземпляром объекта. Давайте посмотрим, как это сделать в Typescript.

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

Проверка типов во время выполнения в Typescript

Выявленной основной проблемой в настоящее время является отсутствие проверки типов во время выполнения. Массив students в нашем файле данных может содержать что угодно, и наш код будет передавать его так, как если бы он был правильным, когда это не так. Защитное программирование говорит, что мы должны очистить, иначе говоря, нормализовать данные перед их использованием.

Чтобы нормализовать данные, наш код должен обрабатывать эти случаи:

  • Все поля существуют и правильно отформатированы
  • Поле пол должно быть проверено на наличие всех правильных значений пола
  • Числовые поля должны содержать либо числовые, либо строковые значения и сохранять поле в виде числа
  • Он должен обнаруживать поля, которые имеют совершенно сумасшедшие значения
  • Он должен обнаруживать недостающие поля

Скопируйте interface2.ts в interface3.ts и приготовьтесь вносить изменения.

Давайте начнем с создания класса StudentImpl для реализации интерфейса Student. Не пахнет ли это “бывшим программистом Java”, чтобы назвать класс StudentImpl ? Какое уродливое имя класса, но это обычная практика в Java.

Если бы мы просто использовали это:

class StudentImpl implements Student {
 id: number;
 name: string;
 entered: number;
 grade: number;
 gender: Gender;
};

Мы ничего не выиграем, потому что нет никакого принудительного исполнения чего-либо во время выполнения.

В документации Typescript рекомендуется, чтобы в подобном случае данные хранились в закрытом поле и использовались функции доступа get/set.

Теперь определение класса будет следующим:

class StudentImpl implements Student {
 private \_id: number;
 private \_name: string;
 private \_entered: number;
 private \_grade: number;
 private \_gender: Gender;

 get id(): number { return this.\_id; }
 set id(id: number) { this.\_id = id; }
 .. similar pattern for the other fields
};

Но это не учитывает следующее:

  • Случай, когда YAML использовал строку, а не число
  • Плохо отформатированный номер
  • Отсутствующее поле

После довольно продолжительных экспериментов мы разработали это определение класса:

class StudentImpl implements Student {
 constructor(id: number | string, 
             name: string, 
             entered: number | string,
             grade: number | string, 
             gender: string) {
   this.setID(id);
   this.setName(name);
   this.setEntered(entered);
   this.setGrade(grade);
   this.setGender(gender);
 }
 private \_id: number;
 private \_name: string;
 private \_entered: number;
 private \_grade: number;
 private \_gender: Gender;

 get id(): number { return this.\_id; }
 set id(id: number) { this.setID(id); }
 setID(id: number | string) {
   this.\_id = normalizeNumber(id, 'Bad ID');
 }
 get name() { return this.\_name; }
 set name(name: string) { this.setName(name); }
 setName(name: string) {
   if (typeof name !== 'string') {
     throw new Error(`Bad name: ${util.inspect(name)}`);
   }
   this.\_name = name; 
 }

 get entered(): number { return this.\_entered; }
 set entered(entered: number) { this.setEntered(entered); }
 setEntered(entered: number | string) {
   this.\_entered = normalizeNumber(entered, 'Bad year entered'); 
 }

 get grade(): number { return this.\_grade; }
 set grade(grade: number) { this.setGrade(grade); }
 setGrade(grade: number | string) {
   this.\_grade = normalizeNumber(grade, 'Bad grade');
 }

 get gender(): Gender { return this.\_gender; }
 set gender(gender: Gender) { this.setGender(gender); }
 setGender(gender: string | Gender) {
   this.\_gender = parseGender(gender);
 }
}

В этом случае шаблон для каждого поля является:

  • Объявить хранилище данных как частное поле в определении объекта
  • Объявите простую функцию получения для доступа к этому полю
  • Объявите простую функцию setter, которая вызывает setFieldName
  • Объявите функцию с именем setFieldName , которая проверяет данные перед их сохранением в поле

Вы должны заметить странность с типом параметра в setFieldName методах. Подожди, мы еще вернемся к этому.

У нас также есть конструктор , который поможет в создании экземпляров объектов. Чтобы использовать constructor , в классе реестра измените students getter на этот:

get students(): Student[] {
 if (this.\_parsed) {
   let ret: Student[] = [];
   for (let student of this.\_parsed.students) {
     try {
**ret.push(new StudentImpl(  
         student.id, student.name,   
         student.entered, student.grade,   
         student.gender));**  
     } catch (e) {
       console.error(`Could not convert student ${util.inspect(student)} because ${e}`);
     }
   }
   return ret;
 }
}

Другими словами, вместо того, чтобы помещать анонимный объект в массив, мы помещаем StudentImpl.

Давайте теперь поговорим о параметре для методов setFieldName :

_setFieldName_(grade: number | string) { .. }

Это функция Typescript, называемая Union Types . Там, где в коде написано “grade: number | string”, вы должны прочитать это как ” параметр grade может иметь либо тип number, либо тип string “.

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

В этом приложении у нас есть файл данных, где эти поля могут легко быть либо числом, либо строкой. В вопросах, перечисленных ранее, мы говорили, что числовые поля должны быть инициализируемыми либо числом, либо строковым значением и храниться в виде числа. Определение типа параметра (тип объединения) является первым шагом к реализации этой цели. Второй шаг – это функция normalizeNumber, которую мы сейчас увидим, которая также должна использовать тип объединения и обрабатывать преобразование из любого в число при проверке типа для обеспечения правильности.

В идеале, средства доступа “set” было бы достаточно, и нам не потребовалась бы эта третья функция. Но компилятор Typescript не позволил этому случиться, и поэтому нам пришлось ввести эту третью функцию. Но должны ли мы помнить, что всегда нужно вызывать эту третью функцию?

Мы были немного хитры. Каждый установщик идет дальше и вызывает соответствующую функцию setFieldName . Проверяя скомпилированный код, мы видим, что из-за отсутствия проверки типа во время компиляции установщик в конечном итоге поступит правильно:

get entered() { return this.\_entered; }
set entered(entered) { this.setEntered(entered); }
setEntered(entered) {
 this.\_entered = normalizeNumber(entered, 'Bad year entered');
}

Как мы уже знаем, во время выполнения код JavaScript не применяет (как мы видим здесь) типы, записанные в коде Typescript. Поэтому независимо от того, какой тип мы предоставили установщику, он будет передан соответствующей функции setFieldName , и проверка типа во время выполнения будет выполнена, обеспечивая требуемую безопасность.

Мы допустили оплошность, не рассмотрев необходимые функции для проверки и преобразования типов во время выполнения.

function normalizeNumber(num: number | string,
                         errorIfNotNumber: string): number {
  if (typeof num === 'undefined') {
    throw new Error(`${errorIfNotNumber} — ${num}`);
  }
  if (typeof num === 'number') return num;
  let ret = parseInt(num);
  if (isNaN(ret)) {
    throw new Error(`${errorIfNotNumber} ${ret} — ${num}`);
  }
  return ret;
}

function isGender(gender: any): gender is Gender {
 return typeof gender === 'string'
    && (gender === 'male' || gender === 'female');
}

function parseGender(gender: string): Gender {
 if (!isGender(gender)) throw new Error(`Bad gender: ${gender}`);
 return (gender === 'male') ? Gender.male : Gender.female;
}

В normalizeNumber мы выполняем различные проверки и либо возвращаем число, либо выдаем ошибку. Это зависит от поведения функции parseInt, где, если она не может найти анализируемое число во входных данных, она просто возвращает NaN. Проверив наличие NaN, мы автоматически обнаружили целый ряд возможных состояний ошибки.

Аналогично при разборе пола мы выполняем различные проверки и либо возвращаем пол, либо выдаем ошибку.

Ключевым приемом здесь является то, что Typescript называет защитой типов. Это выражения времени выполнения, которые гарантируют, что тип переменной соответствует ожидаемому. Защита типа для перечисления gender – это функция is Gender, показанная в коде. Возвращаемый тип в is Gender, ” foo – тип “, является логическим значением, true или false, указывающим, соответствует ли именованный параметр этому типу.

В функции interpolateNumber у нас есть встроенный тип guard:

if (typeof num === 'number') return num;

В другом месте документации Typescript говорится, что компилятор распознает этот шаблон как защиту типа. Он распознает оба типа и instanceof выражения для этой цели.

Защита типов работает рука об руку с выводом типов , выполняемым компилятором Typescript. Компилятор выполняет обширный статический анализ кода во время компиляции. Когда он распознает этот шаблон, он может определить допустимые типы для каждой переменной.

Но это не уникально для Typescript. Уберите типы, и у вас будет обычный код JavaScript, который вы использовали бы для защитного программирования. Защита типов – это просто способ реализации проверки типов во время выполнения, которую мы должны выполнять в любом случае. Как мы отмечали ранее, большинство из нас не пишут код с такой защитой. Вполне вероятно, что мы получим достаточную выгоду от написания кода способом Typescript, защиты типов и всего остального, что у нас будет стимул фактически внедрить защиту типов.

Теперь мы получаем это:

$ ts-node interface3.ts students.yaml 
(node:10048) ExperimentalWarning: The fs.promises API is experimental
Could not convert student { id: 3,
 name: 'Nobody',
 entered: 'yesterday',
 grade: 'lines',
 gender: 'None-of-your-Business' } because Error: Bad year entered NaN — yesterday
1 John Brown entered: 1977 grade: 4 gender: male
2 Juanette Brown entered: 1977 grade: 4 gender: female

У нас есть проверка типов во время выполнения. В коде есть определения типов в каждом углу, которые не только помогают компилятору Typescript, но и помогают будущим разработчикам этого кода понять, что к чему.

В настоящее время результатом является то, что наш код способен нормализовать две записи учащихся (которые могут быть нормализованы), и он отмечает ошибку в третьей, потому что она не может быть нормализована.

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

Вывод

Хотя мы только коснулись поверхности Typescript, мы увидели достаточно, чтобы оценить его полезность. Будет ли программисту на Java или C# достаточно комфортно писать большую систему?

До тех пор, пока программист понимает свои ограничения, Typescript является очень полезным языком программирования.

В каждой области Typescript предлагает функции, выходящие за рамки того, что обычно доступно на других языках. Объявления типов – это надмножество того, что предлагают другие языки, равно как и объявления классов и интерфейсов. Определения классов Typescripts являются надмножеством того, что было добавлено в JavaScript в ES-2015. Мы не касались модуля и пространство имен features, оба из которых являются надмножеством того, что доступно в обычном JavaScript.

Другими словами, набор функций Typescripts выходит за рамки того, к чему люди привыкли на других языках или в JavaScript.

Программисту на Java или C# будет удобно использовать классы Typescript для описания иерархии классов, с помощью которой можно организовать свой код.

Основным недостатком Typescript является то, что его проверка типов выполняется только во время компиляции. Как мы видели, проверка типов во время выполнения отсутствует, и вместо этого у нас есть накладные расходы на самостоятельное кодирование. Программисты, использующие Java, C# или другие языки, не имеют таких накладных расходов.

Плагин: Blog rocket, видеорегистратор для веб-приложений

Плагин: Blog rocket, видеорегистратор для веб-приложений

Log Rocket – это инструмент ведения журнала интерфейса, который позволяет воспроизводить проблемы так, как если бы они происходили в вашем собственном браузере. Вместо того, чтобы гадать, почему возникают ошибки, или запрашивать у пользователей скриншоты и дампы журналов, Log Rocket позволяет воспроизвести сеанс, чтобы быстро понять, что пошло не так. Он отлично работает с любым приложением, независимо от фреймворка, и имеет плагины для регистрации дополнительного контекста из Redux, Vuex и @ngrx/store.

В дополнение к ведению журнала действий и состояний Redux, Log Rocket записывает журналы консоли, ошибки JavaScript, трассировки стека, сетевые запросы/ответы с заголовками + тела, метаданные браузера и пользовательские журналы. Он также использует DOM для записи HTML и CSS на странице, воссоздавая видео с идеальными пикселями даже в самых сложных одностраничных приложениях.

Попробуйте это бесплатно.

Оригинал: “https://dev.to/bnevilleoneill/is-typescript-on-node-js-good-enough-for-java-developers-4m8e”