Многие ошибки, которые запускаются в приложениях JavaScript, неожиданно возникают из-за непредвиденного или неправильного использования типов и значений null или undefined, которые, в свою очередь, вызывают TypeErrors или ReferenceErrors. Неперехваченные ошибки приводят к сбою приложений и вынуждают разработчиков находить места ошибок и исправлять их, что, очевидно, требует времени, которое в противном случае можно было бы потратить более продуктивно.

TypeScript при правильном использовании позволяет нам защититься от вышеупомянутых проблем. Чтобы понять, как мы можем защитить себя от проблем с допустимостью значений NULL, нам нужно сначала понять, как был разработан JavaScript в отношении обработки null и undefined.

Неопределенный тип

Несмотря на то, что JavaScript можно рассматривать как язык с динамической типизацией (что в конечном итоге означает, что тип связан со значением переменной, а не с самой переменной), у него есть типы под маской. Тип undefined (некоторые люди могут предпочесть обозначение, начинающееся с заглавной буквы) допускает только одно значение: undefined, которое также является одним из неизменяемых примитивов JavaScript и, как оказалось, является глобальным объектом. Я хотел бы обсудить два основных использования этого типа.

Не определяется как отсутствие собственности

Давайте определим переменную x.

let x = {};

Если мы попробуем сослаться на x.y, мы получим undefined. В этом контексте y не существует (или undefined) как свойство переменной x. Это не означает, что x.y существует и удерживает undefined, хотя может рассматриваться как таковой.

Не определено как значение переменной

Давайте снова определим переменную x.

let x = undefined;

Переменная x теперь удерживается undefined. Он также может иметь другую ценность в будущем, если в этом возникнет необходимость.

Оператор пустоты

Вкратце, в JavaScript есть оператор void, который всегда возвращает undefined. Это полезно при минимизации JS-кода, поскольку мы можем заменить каждый undefined на void 0 и по-прежнему получать тот же результат во время выполнения.

Нулевой тип

Тип null - это тип, допускающий только одно значение, значение null, которое также является неизменяемым примитивом. Использование относительно простое:

let y = null;

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

Нулевой нужен?

Внимательный читатель может понять на этом этапе, что null использования можно заменить на undefined, когда мы хотим выразить, что переменная не имеет значимого значения (что в корне отличается от no value вообще, что является проблемой в JavaScript, поскольку язык, похоже, не имеет формального нижнего типа). Вышеупомянутый подход был рекомендован некоторыми людьми (например, команда TypeScript здесь, есть правило TSLint здесь, и Дуглас Крокфорд говорил об этом здесь), однако при его реализации можно столкнуться с определенными проблемами:

  • сторонние библиотеки могут использовать как null, так и undefined и присваивать им разные значения, поскольку это соответствует потребностям задач, решаемых этими библиотеками,
  • DOM использует null,
  • В формате JSON нет явного понятия undefined, но есть понятие null (все свойства со значениями undefined теряются во время сериализации, что не относится к значениям null),
  • Базы данных SQL поддерживают значение theNULL для своих полей, что может мешать разработчикам, создающим ORM.

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

Обработка TypeScript допустимости значений NULL

Сам TypeScript является языком со статической типизацией, однако он позволяет отключать управление типом с помощью типов any и unknown. Создатели решили, что слово null всегда будет использоваться в связи с типами и значениями null и undefined, что является разумным выбором, поскольку мы не должны использовать термин нижние типы поскольку эти два типа содержат значения (несмотря на то, что эти значения не значимы).

Если параметр конфигурации strictNullChecktsconfig.json) установлен в false, тогда типы null и undefined всегда являются подтипом любого другого определенного типа, что приводит к тому, что следующий оператор является допустимым:

let x: string = null;

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

Параметры с strictNullCheck по true разделяют типы null и undefined и остальные типы, что делает следующие инструкции недопустимыми:

let x: string = null;
let y: string = undefined;

Определение полей, допускающих значение NULL

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

type A = {
    x: string | null;
};
type B = {
    x: string | undefined;
};
type C = {
    x?: string;
};
type D = {
    x?: string | undefined;
};
type E = {
    x?: string | null
};
type F = {
    x?: string | null | undefined;
};

Тип A тривиален, поскольку он позволяет свойству x содержать значение, которое может быть либо string, либо null.

Типы B, C и D идентичны в способах обработки значений соответствующих свойств, однако есть одно небольшое различие. Модификатор ? гарантирует, что свойство не может быть установлено в качестве ключа на уровне объекта. Это означает, что экземпляр типа B никогда не бывает пустым объектом, тогда как экземпляры типов C и D могут быть пустыми. Единственное различие между C и D состоит в том, что тип D явно указывает, что свойство x может содержать undefined, однако для большинства разработчиков они будут взаимозаменяемыми (для справки, пожалуйста, ознакомьтесь с этим правилом линтинга).

Типы E и F расширяют предыдущие идеи с помощью значений null.

Нижний тип

Как уже упоминалось ранее, нижний тип - это тип, который не имеет присваиваемых значений внутренне, и поэтому типы null и undefined не являются формально нижними типами, хотя в разговорной речи они могут рассматриваться как таковые в сообществе JavaScript. Тип void, как умный псевдоним undefined, не является нижним типом. TypeScript определяет только один нижний тип (одного достаточно): тип never, который часто используется при работе с невозможными сценариями (например, функция, которая всегда генерирует исключение, никогда не возвращает никакого значения, следовательно, его тип возврата - never).

Без значения Nullable

TypeScript определяет специальный тип как часть своей стандартной библиотеки:

type NonNullable<T> = T extends null | undefined ? never : T;

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

Давайте объявим один вспомогательный тип и массив:

type NullableNumber = number | null | undefined;
const nullableNumbers: ReadonlyArray<NullableNumber> = [1, 2, 3, null, undefined];

nullableNumbers - это набор чисел, null и undefined. Мы должны сразу избавиться от значений, допускающих значение NULL, с помощью простой функции filter, верно?

const numbers: ReadonlyArray<number> = nullableNumbers
    .filter(n => n !== null && n !== undefined);

Неправильный! Оказывается, компилятор TypeScript не понимает тонкого сужения типа здесь и выдает ошибку времени компиляции, хотя это совершенно нормально во время выполнения (даже если это будет работать, компилятор должен понять, почему). Мы можем помочь компилятору понять, что мы здесь сделали, используя тип NonNullable с защитой типа:

const isNonNullable = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;
const nonNullableNumbers: ReadonlyArray<number> = nullableNumbers.filter(isNonNullable);

Теперь код компилируем и позволяет нам отфильтровывать значения, допускающие значение NULL.

Резюме

Надеюсь, вам понравилось читать статью. Обратите внимание, что это всего лишь краткое введение в довольно сложную проблему, предназначенную только для TypeScript. Изобретатель ссылки 69_ Тони Хоар считает это ошибкой. В настоящее время концепция допустимости значений NULL по-прежнему используется различными основными языками, включая C, C ++, Java или Python, однако создатели некоторых языков прилагают постоянные усилия, чтобы препятствовать доступу к голым указателям и пустым ссылкам, используя довольно сложные методы (например, ссылка в C ++ всегда гарантированно содержит значимое значение).