Обучающих статей, рассматривающих различия между null и undefined на просторах интернета столько, что, кажется, и int64 не хватит, чтобы их сосчитать. Так что вводные бла-бла-бла оставим за скобками и рассмотрим основные подводные камни, которые бросает нам под ноги undefined.

Отказ от ответственности

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

Неинициализированные переменные

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

/**
 * @param {number} op1
 * @param {number} op2
 * @returns {number}
 */
function sum(op1, op2) {
  return op1 + op2;
}

/**
 * @type {number}
 */
let a; // undefined

// ...

if (condition) { // это условие не выполнилось
  a = 10;
}

/**
 * @type {number}
 */
let b = 10;

const result = sum(a, b);

a = 1; // поздно

console.log('sum =', result); // sum = NaN

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

function sum(op1, op2) {
	return op1 + op2;
}

let a;

(async () => {
	a = await getRemoteAValue();
})();

let b = 10;

const result = sum(a, b); // очевидно выполнится до того, как переменная a будет инициализирована

console.log('sum =', result); // sum = NaN

Пример выше — конечно, явная ошибка, нужно сперва дождаться инициализации a, а потом уже получать сумму. Тем не менее, на практике я подобных ошибок встречал немало, особенно у тех, кто еще не до конца понял принципы работы асинхронности.

Сериализация/десериализация

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

Возьмем очень простой объект, состоящий из трех полей (для удобства, структура объекта будет описана в виде интерфейса TypeScript):

interface SimpleObject {
  id: number;
  name: string | null;
  desc?: string;
}

Теперь создадим объект, соответствующий интерфейсу выше:

const obj = { id: 1, name: null, desc: undefined };

Конечно, можно было объявить еще короче, не указывая поле desc вовсе, но мы нарушим все рекомендации комитетов по стандартизации и сделаем по-своему. Давайте теперь обойдем объект в цикле по ключам:

for (const key in obj) {
  console.log(`key: ${key}, value: ${obj[key]}`);
}

Результат:

key: id, value: 1
key: name, value: null
key: desc, value: undefined

Прекрасно, теперь сериализуем его в JSON, передадим по сети, десериализуем и вновь выполним обход:

const serialized = JSON.stringify(obj);
// где-то здесь происходит передача
// код далее как бы выполняется на другой стороне соединения
const incoming = JSON.parse(serialized);

for (const key in incoming) {
  console.log(`key: ${key}, value: ${obj[key]}`);
}

Результат:

key: id, value: 1
key: name, value: null

Уппс! Погодите-ка, а куда же делось наше поле desc? Дело в том, что в JSON нет типа undefined, для него поле со значением undefined - это отсутствующее поле, если вывести значение serialized — получим следующий текст:

{"id":1,"name":null}

При этом десериализованный объект (incoming) все еще соответствует интерфейсу SimpleObject, так как попытка получить значение поля desc в любом случае даст значение undefined:

console.log(obj.desc); // undefined
console.log(incoming.desc); // undefined

В 90% случаев все будет нормально и не будет являться проблемой ровно до той поры, пока мы не попытаемся произвести сравнение этих объектов. Создадим копию объекта obj и, при помощи lodash.isEqual сравним копию и десериализованный результат с оригиналом:

import isEqual from 'lodash/isEqual';

const objCopy = { ...obj }; // не будем заморачиваться с глубоким копированием, так как все поля нашего объекта - примитивы

console.log(isEqual(obj, incoming)); // false
console.log(isEqual(obj, objCopy)); // true

Какая неприятная неожиданность! Эта неприятность может нас поджидать во многих случаях, например, если мы хотим сохранить сериализованное значение где-либо, а потом подгрузить и сравнить с текущим (redis, РСУБД, файл, localStorage).

Решается тривиально: использовать null для значений полей вместо undefined. К сожалению, это возможно не всегда. Например, если удаленный ресурс вам не принадлежит и уже использует опциональные поля. В таком случае, банального isEqual будет уже недостаточно — придется добавить дополнительных проверок, либо добавлять undefined-поле сразу после десериализации сохраненного значения. Другими словами, требуется провести валидацию десериализованных данных.

Вывод

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

Ссылка на репозиторий с примерами.

P.S.: Ничего не имею против минусов, но, пожалуйста, будьте добры пояснить - в чем именно заключается низкий технический уровень материала. Заранее, спасибо.

Комментарии (83)


  1. space2pacman
    07.12.2022 19:46
    +7

    Статья для кто работает с JavaScript без документации?


    1. Devoter Автор
      07.12.2022 20:05
      -4

      Вы прочитали всю актуальную спецификацию ECMAScript? Но, в целом, да, и ремарка насчет игнорирования рекомендаций комитета на это намекает. Реальность не так идеальна, как нам хотелось бы.


      1. MaryRabinovich
        07.12.2022 21:19
        -1

        Я бы хотела вас поддержать. Документация документацией, но она толстая. Мало кто всё от корки до корки прочёл.

        Я обожаю копаться в доке (и не только в ней), но никогда не сталкивалась с десериализацией данных с исходными undefined. Вернее, со сравнением объектов после сериализации-десериализации при таких данных.

        Словила инсайт. Спасибо:)


        1. GothicJS
          07.12.2022 22:14
          +2

          но никогда не сталкивалась с десериализацией данных с исходными
          undefined. Вернее, со сравнением объектов после
          сериализации-десериализации при таких данных.

          А вы для себя подумайте - почему вы с таким не сталкивались.


        1. Ilusha
          07.12.2022 23:21
          +6

          А как же сериализация, когда вы отправляете данные на бэк?)
          null - поле отправится, undefined - будет отфильтровано.


  1. shai_hulud
    07.12.2022 19:51
    -10

    помощи lodash.isEqual сравним копию и десериализованный результат с оригиналом:

    Дак это же проблема какой-то библиотеки.

    undefined не хуже и не лучше null, оба ИМХО не нужны в современной ЯП, но что поделать.


    1. Devoter Автор
      07.12.2022 20:08

      Насчет первого - нет, это не проблема библиотеки.

      Насчет второго - поясните, позволю с вами не согласиться. null нужен, иначе как вы укажете на явное отсутствие значения, например, отсутствие объекта в переменной, который может там быть, а может и не быть.


      1. rsashka
        07.12.2022 20:51
        +8

        Вообще то null, это тоже значение


        1. mozg3000tm
          08.12.2022 23:38

          Смотря что вы называете значением. В типизированных языках (с++), ссылочные типы (точнее переменные/объекты) хранят ссылку на область памяти где находятся данные, которые и есть значение. Но null представляет собой отсутствие такой ссылки на область памяти и никуда не ссылается и в упомянутом смысле не является значением. Это отсутствие ссылки на значение или ссылка в никуда.


      1. vabka
        07.12.2022 22:12
        +3

        Насчет второго - поясните, позволю с вами не согласиться. null нужен, иначе как вы укажете на явное отсутствие значения, например, отсутствие объекта в переменной, который может там быть, а может и не быть.

        Для этого во многих языках существует Optional / Maybe где ты можешь явно на уровне типов разрулить, возможно ли отсутствие данных.

        Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.

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


        1. Devoter Автор
          07.12.2022 22:36
          -1

          Не силен в функциональных языках, так что спрошу: если поле/переменная Optional, то каково будет ее значение, если там нет ничего? Разве не тот же null?


          1. vabka
            07.12.2022 23:11
            +1

            Нет, будет None.

            Там же нет null ;)


            1. Devoter Автор
              07.12.2022 23:14

              Это шутка такая? Чем None отличается от null? В C++ есть nullptr, к примеру, а в Go - nil.


              1. TheTryProgrammerName
                07.12.2022 23:44
                +2

                Тем, что мы явно это обрабатываем.
                Не возникает ситуаций, когда что-то где-то вернуло null, а мы забыли проверку и всё пошло по одному месту. Если не используем Optional - точно знаем, что None не будет, если используем - то обрабатываем варианты: когда None и когда Some.


                1. Devoter Автор
                  07.12.2022 23:49
                  +1

                  Это называется Sound null safety и никакого отношения к моему комментарию не имеет. Речь о том, что значение null/nil/none все равно есть. Изначально я в ветке оспаривал это утверждение:

                  undefined не хуже и не лучше null, оба ИМХО не нужны в современной ЯП, но что поделать.


                  1. 0xd34df00d
                    08.12.2022 06:08
                    +5

                    null принадлежит любому типу (насколько это высказывание вообще имеет смысл для js), nullptr принадлежит любому типу-указателю. Nothing принадлежит только Maybe.


                    1. nin-jin
                      08.12.2022 06:49
                      +2

                      1. 0xd34df00d
                        08.12.2022 08:12
                        +1

                        А 42 + null === 42, и что?


                      1. nin-jin
                        08.12.2022 08:49
                        -2

                        Подсказка: typeof NaN === 'number'


                      1. 0xd34df00d
                        08.12.2022 08:53
                        +3

                        Очень хорошо. Давайте теперь вернёмся к исходному тезису о разнице между null/nullptr и Nothing.


        1. funca
          07.12.2022 23:17
          +4

          Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.

          У автора вылезла проблема при сериализации структуры данных в JSON. Как вы посоветуете представлять в этом формате Optional? А для ситуации когда они вложены друг в друга?


          1. 0xd34df00d
            08.12.2022 06:09
            +1

            Брать и представлять, вроде Maybe String и так далее. В чем конкретно ваш вопрос?


            1. dopusteam
              08.12.2022 07:34
              +1

              Покажите как будет JSON выглядеть с Optional полем. А ещё с полем, тип которого Optional, вложенный в другой Optional


              1. 0xd34df00d
                08.12.2022 08:11
                +1

                Покажите как будет JSON выглядеть с Optional полем.

                Зависит от настроек сериализатора. Могу сделать так, что первый же отсутствующий Maybe будет, ээ, отсутствовать и в JSON.

                А ещё с полем, тип которого Optional, вложенный в другой Optional

                После того, как вы null в null вложите.


                1. dopusteam
                  08.12.2022 08:44

                  Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.

                  После того, как вы null в null вложите

                  Вы ж сами хвастались этим. На что разумно сказали, что у автора подгорело от сериализации, так что вернули разговор в русло json. Так что ветку читайте внимательнее


                  1. 0xd34df00d
                    08.12.2022 08:52
                    +2

                    Для разных юзкейсов я буду выбирать разные форматы. Если мне нужно представлять Maybe (Maybe Ty), то одним полем не обойтись (но тогда и у исходного автора проблемы), но если не нужно, то я могу оставить совместимость с предложенной схемой и иметь на своей стороне нормальный Maybe. Всё ещё не понимаю контр-аргумент, короче.


                    1. funca
                      08.12.2022 13:14
                      +1

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


      1. shai_hulud
        08.12.2022 02:32

        null нужен, иначе как вы укажете на явное отсутствие
        значения, например, отсутствие объекта в переменной, который может там
        быть, а может и не быть

        Для рантайма option, для сериализации с непостоянной структурой просто не будет поля, с постоянной добавляется второе поле XXXisNull: true/false либо на всю структуру битовая маска какие поля заполнены.

        Опять же это не для всех ЯП. В тех в которые вотнули null уже ничего не поделаешь, везде будут проверки на != Null


        1. Devoter Автор
          08.12.2022 03:08

          А как работает option без значения null? Должен быть тогда какой-то оператор который возвращает true или false в зависимости от наличия/отсутствия значения, а также какая-то встроенная функция или оператор, которая позволяет выставить поле в неопределенное. Тогда это тоже самое, только без явного наличия значения.


          1. HelpOrMe
            08.12.2022 04:35

            В том что Optional это отдельный тип. Ничего кроме Optional не может быть None, нельзя взять, например, вектор и передать его аргументом в функцию как None. Если же функция подразумевает, что значение аргумента может отсутствовать, то тип параметра будет Optional<Vec<...>>, и чтобы получить значение с Optional тебе надо использовать паттерн матчинг или один из миллиона удобных методов, например unwrap_or_default(). Тут надо специально постараться, чтобы не заметить отличие от обычного null, коим может являться все что угодно.


            1. Devoter Автор
              08.12.2022 04:44

              Понятно, но, думаю, вы все-таки путаете null safety и отсутствие null как такового. То, что вы описали, примерно то, что я представлял. Если не учитывать различные небезопасные преобразования, то тот же TypeScript также не позволит записать null в поле конкретным типом, как и undefined. Но если TS - надстройка, то тот же Dart вполне четко не позволяет использовать null для переменных и полей, которым явно этот тип не разрешен. Так что, принципиальной разницы нет. Другое дело, что динамические языки с null (тот же JS) позволяют его пихать куда угодно, что, безусловно, усложняет ситуацию.


              1. shai_hulud
                08.12.2022 12:34
                +1

                Мне кажется вы путаете null и отсуствие значения. т.к. null это значение, а Optional -> None это тип, который указывает на отсутствие значения.


                1. funca
                  08.12.2022 13:03

                  Просто в языках с динамической типизацией, к коим относится и JS, типы связаны со значениями в рантайме и по-другому не бывает.

                  В теории, если разделять понятия Model of computation и Execution model, то можно говорить о корректности программы - с точки зрения алгоритма (где для проверки на помощь приходит логика, в т.ч. автоматика в виде статического код анализа и тайп чекеров), и с точки зрения рантайма - как о связанных, но не тождественных понятиях. В этом смысле система типов в TS и особенности исполнения JS вещи ортогональные.


  1. GothicJS
    07.12.2022 21:13
    +3

    Так что вводные бла-бла-бла оставим за скобками и рассмотрим основные подводные камни, которые бросает нам под ноги undefined.

    Просто ради интереса вопрос к автору: из какого языка вы дошли до JS ?

    Теперь создадим объект, соответствующий интерфейсу выше:

    const obj = { id: 1, name: null, desc: undefined };

    А какой логикой вы руководствуетесь, когда ставите в качестве значения undefined ? Почему одно свойства null, а другое undefined ?


    1. MaryRabinovich
      07.12.2022 21:20
      -2

      Потому что блин оно само undefined, когда переменная создана, но ничего не вписано. С какого причём языка сюда не приди.

      (попробую пофантазировать) У вас где-то есть отдельные id, name и desc. Вы обустраиваете их в объект через {id, name, desc}. Если desc задана, но не инициализирована, она попадёт в объект как undefined.


      1. GothicJS
        07.12.2022 22:17
        +1

        Если desc задана, но не инициализирована, она попадёт в объект как undefined.

        А кто виноват в том, что она задана, но не инициализирована?


        1. Devoter Автор
          07.12.2022 22:46
          +1

          Вы говорите о причинах, в тексте написано - как такой ситуации избежать (используйте null), но рассматривается вариант, который реально возможен и встречается в проектах. Оценки плохо/хорошо тут нет. Это может быть сторонняя библиотека, автор которой посчитал, что использование undefined вместо null - отличная идея. Ситуация может возникнуть в результате разных обстоятельств: функция, которая должна вернуть объект - вернула undefined, а мы записываем это значение в поле нашего объекта, и да, в таком случае, правильно было бы обработать этот результат, скажем так:

          const prepared = {
            internal: getAnotherObject() ?? null
          };

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


      1. aamonster
        08.12.2022 01:31
        +1

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

        Так что "с какого языка ни приди" – неточно.


  1. dynamicult
    07.12.2022 21:41
    +6

    Прекрасно, теперь сериализуем его в JSON, передадим по сети, десериализуем и вновь выполним обход

    А ещё в JSON нет типа callable object, это значит, что функции вредны и использовать их нельзя? Или они причиняют вам боль? Что насчет BigInt, Symbol и пользовательских типов?

    Вы сами нарушили контракт в момент когда решили объявитьdesc? как string | undefined. Потому что первое является соглашением о наличии или отсутсвии поля в объекте, а второе определяет тип его значения. И это не одно и то же. Объекты - не структуры. У них есть цепочка наследования и поля могут перекрываться.

    А для контроля сериализации в JSON конкретного типа объекта есть перегрузка toJSON.


    1. Devoter Автор
      07.12.2022 22:57

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

      С точки зрения интерпретатора что desc?: string;, что desc: string | undefined равносильны, если подскажете способ различить эти определения, кроме как визуально - буду рад. Что касается различных схем, сериализаторов и так далее - ясно дело, что решения есть, правда, чем именно перегрузка toJSON может вам помочь мне неведомо, если вы имеете ввиду, что можно сериализовать undefined-поле как null, то это как раз далеко не то, что нужно, если конктракт подразумевает наличие опционального, а не nullable-поля.


      1. Ilusha
        07.12.2022 23:18

        С точки зрения интерпретатора что desc?: string;, что desc: string | undefined равносильны

        // b = обязательное поле
        const example1 = { a: 1 , b: undefined } as { a: number, b: string | undefined }
        example1.hasOwnProperty('b') === true
        Object.keys(example1).length === 2

        // b = опциональное поле
        const example2 = { a: 1 } as { a: number, b?: string }
        example2.hasOwnProperty('b') === false
        Object.keys(example2).length === 1


        1. Devoter Автор
          07.12.2022 23:23

          И что доказывает ваш пример? Что если объявить два разных объекта по-разному, что будет разный результат? Что мешает написать const example2 = { a: 1, b: undefined } as { a: number; b?: string }? К тому же, вы вновь аппелируете к TypeScript, когда речь в статье о JS.

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


          1. Ilusha
            07.12.2022 23:53

            Вы задали вопрос:

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

            Я на него ответил. Как с позиции TS, так и с позиции JS.


            1. Devoter Автор
              07.12.2022 23:58

              С точки зрения TS - только в одну сторону, а с точки зрения JS - я этот вариант (с проверкой наличия ключа) как единственный способ проверить и привел в тексте публикации, но, хорошо, аргумент принят. Статью поправлю.

              Upd: ради интереса проверил, если использовать as, то TS не ругается:

              const example2 = { a: 1 } as { a: number; b: string | undefined };


              1. Ilusha
                08.12.2022 00:12

                type A = { a: number, b?: number }
                type B = { a: number, b: number | undefined }

                const a:A = { a: 1 }
                const test = (obj: B):B => obj
                test(a) // TS2345: Argument of type 'A' is not assignable to parameter of type 'B'.   Property 'b' is optional in type 'A' but required in type 'B'.

                как единственный способ проверить 

                Вы сами писали рядом

                невозможно отследить изменение свойства, если оно отсутствует в объекте (тот же Vue 2.x).

                Поэтому важно понимать, в чем различие контрактов.
                А если говорить про vue2, то там распространенный кейс получить plain object: JSON.parse(JSON.stringify(obj)). Не знаю, что там будет с undefined значениями.

                Второй способ: итерирование по ключам. И здесь это тоже важно.


                1. Devoter Автор
                  08.12.2022 00:24

                  Мой комментарий ниже, вроде как, дополняет предыдущий. Через объявление типа через двоеточие да, будет ругаться. Если же привести к типу через as - не будет, так что даже тут TS не спасет в 100% случаев, не говоря уже о том, что только что десериализованный объект имеет тип any, который приводится к чему угодно, так что дополнительных проверок не избежать. Такое чувство, что вы пытаетесь найти огрехи в коде, который лишь является упрощенной иллюстрацией возможного развития событий. Думаю, если бы я описал еще более подробно, вы бы сказали, что воды столько, что читать вообще невозможно. Что касается stringify+parse, то undefined-поля будут просто удалены, про Vue 2 я имел ввиду всякие Vuex или computed. computed просто не заметит появление отсутствовашего свойства в объекте, даже если он отслеживаемый.


                  1. Ilusha
                    08.12.2022 01:01

                    такое чувство, что вы пытаетесь найти огрехи в коде

                    Да. Потому что Ваша статья будет проиндексирована и люди будут натыкаться на неё. Мы же не на кухне и не в ламповом чатике)

                    Я может где-то груб и душнила, за это простите, но без негатива. Для меня хабр с комментами - это в т.ч. в некотором роде образовательная среда.

                    Type Assertions в ts, имхо, недостаточно строг. Но, возможно, это баланс между функциональностью и строгостью.
                    Но валидность выражения const t = {} as string лично у меня вызывает вопросы.

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

                    ...

                    Никогда на слух не мог воспринять это предложение. Да и при чтении тоже приходится напрягаться.

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


                    1. Devoter Автор
                      08.12.2022 01:14

                      Ну, к слову сказать, вашими и не только стараниями, кое-какие правки и пояснения внес, за что спасибо, конечно. В данном случае вода необходимо, чтобы было понятно пошагово - что же происходит. Просто на примерах не все могут уловить смысл. Вы читаете с позиции того, кто уже знает, как оно работает, и для вас это - вода. А для тех, кто не знает - должно дать понимание ситуации: возможные причины ее возникновения, последствия и варианты обхода/решения.


        1. Devoter Автор
          07.12.2022 23:54

          Если хотели сказать, что при описании типа как {a: number; b: string | undefined } нельзя задать переменной значение { a: 1 }, то стоило так и написать. И все равно, мы упираемся в TypeScript, о котором речи не идет.


      1. dynamicult
        08.12.2022 00:31
        +3

        Давайте будем честны, с дуру можно сделать много чего, но вероятность...

        Язык программирования - это не защита от дурака. Административные проблемы не решаются техническими средствами. А вероятности должны чем-то доказываться.

        Еще раз напишу, в вашем кейсе нет никакой сущестенной разницы, будет у вас там поле с undefined, или например с function. Представьте, что у вас не поле desc, а поле filter, которое хранит либо примитивный селектор в виде строки, либо какой-то компаратор в виде функции.

        const obj = { id: 1, name: null, filter: i => i > 10 }

        Теперь всеь ваш последующий код с перечислением свойств, сереализацией и перечеслением после десереализации будет вести точно так же и undefined тут не при чем.

        С точки зрения интерпретатора что desc?: string;, что desc: string | undefined равносильны

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

        При этом по коду далее вы в целом нарушаете ваше же соглашения. Между сериализацией и десереализацией, где вы обозначаете передачу и получение данных из внешних источников, вы не проводите никакой валидации полученнных данных. После этого констатация фатка того, что вывод при перечеслении до и после различается выглядит как минимум абсурдно. У вас там вообще мог придти в incoming из сети совершенно другой объект, а мог придти вообще не объект, а число. Или просто данные не формате JSON. И поведение вашего кода точно так же было каждый раз различно.

        Моя ремарка о toJSON была для общей информации. Но к ней я могу добавить еще такие вещи: JSON - это не единственный формат сериализации, их есть невероятное множество и каждый из них поддерживает разные типы данных. JSON - это не единственный формат сериализации даже в рамках js кода - есть структурное клонирование объектов для передачи их между разными потоками исполнения, которое вполне поддерживает примитивный тип undefined.

        Основной тезис, который я пытался донести: объекты - не структуры. Не плоские структры с данными. Это сложные динамические сущности, обладающие поведением. И не следует рассматривать их и базовые типы данных в языке, словно единственная их задача это описание типов Record.

        JS -
        это динамический язык прграммирования, с динамической же средой. Он развивался эволюционным путем и все примитивы (любые примитивы языка будь то синтаксические конструкции, семантические, типы данные и прочее) в нем присутствующие, имеют свои собственные кейсы. Как только вы заглянете за ширму борьбы с дураком, о котором вы говорите, вы обнаружите удивительный мир рефлексии и метапрограммирования, перегрузки, Proxy, etc где многие возможности языка, которые вам кажутся нелогичными или странными внезапно находят свои применения.

        Вы зачем-то берете совершенно абсрудный кейс, с нарушенными контрактами исполнения, без валидаци ввода/вывода, и делаете умозрительное заключение о какой-то боли от типа undefined, ссылась при этом на какую-то абстрактную дурость. Таких кейсов с дуракми знаете сколько можно насочинять?

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

        Подводя итог, конкретно ваш контракт, каким бы он не был странным, решается простым, хотя бы вот таким кодом.


        1. Devoter Автор
          08.12.2022 00:42

          По поводу контракта с обязательным полем тут уже в комментариях поговорили, и текст я поправил, действительно, правильней описать именно как опциональное поле. Сам контракт был приведен просто чтобы описать ожидаемую структуру объекта. Причем тут ваши высказывания о том, что объекты это не просто структуры (C++ вот начал очень сильно возмущаться) мне неведомо. Понятное дело, что есть не только JSON, но любые другие сериализаторы с поддержкой опциональных полей могут вести себя ровно так же, тут, как вы верно заметили, нужна валидация. И вариантов тут два: используется готовая библиотека для валидации (хотя кто-то же ее ранее написал), валидация пишется вручную, или если бы было написано "валидируйте входящие данные", то вопроса бы у вас не возникло? Вот в случае написания проверок вручную и стоит знать о такой особенности поведения. И да, это именно нюанс языка, из которого такое поведение становится возможным. Какие возможности дает наличие undefined, мне, правда, неясно.

          Upd: Ах, погодите, в тексте же есть указание на то, что потребуются дополнительные проверки. Или это не валидация?


          1. Devoter Автор
            08.12.2022 01:15
            +1

            Внес в текст некоторые пояснения, в том числе - упоминание валидации. Надеюсь, это улучшит текст.


            1. funca
              08.12.2022 11:51

              по-хорошему Parse, don’t validate.


  1. OpenA
    07.12.2022 21:43
    +1

    не знал что парсер JSON выкидывает поля с undefined, класная фича же, оказывается от него даже польза есть.

    что касается примеров то я честно говоря не очень понял в чем конкретная претензия к undefined. Он и есть как бы "неопределенный тип" js-рантайм понятия не иммеет какой тип будет у let a;, а null - это тип object


    1. Ilusha
      07.12.2022 23:19

      Не только парсер, но JSON.stringify


  1. kichrot
    07.12.2022 21:47

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

    Лично я считаю, что Undefined, в некоторых случаях, крайне полезная штука при отладке кода. :)


    1. vabka
      07.12.2022 22:14
      +1

      Можно пример? И как тогда в куче языков люди живут без undefined?


      1. Serdevel
        08.12.2022 01:41

        Еще страдаем без NaN и Infinity.


        1. vabka
          08.12.2022 02:34
          +3

          NaN и Infinity есть везде, где есть IEEE 754.

          Тоесть везде


  1. hello_my_name_is_dany
    07.12.2022 22:41
    +4

    Ну, потеряли при сериализации мы поле и что? Вы же сами написали в интерфейсе, что поле имеет тип string | undefined. Значит, прежде чем его использовать в качестве строки, надо проверить на undefined


    1. Ilusha
      07.12.2022 23:04

      field: string | undefined подразумевает существование поля. field?: string подразумевает, что поле потенциально отсутствует. Это разные контракты.

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


      1. hello_my_name_is_dany
        07.12.2022 23:29

        В рамках JS это одно и тоже, хотя семантически это разные вещи и соглашусь с вами. Но это не отменяет проверок на undefined.

        И если уж честно говорить, то итерация по ключам это слишком грязный хак для решения какой-либо проблемы. Если нет поля, значит и нет значения, тем более раз по интерфейсу так предполагается. Может на фронте часто такие задачи, но на беке редко приходится "рефлексировать".

        Рефлексия очень заманчива, но не стоит ею увлекаться. И если использовать, то только тогда, когда есть уверенность на 100% в результатах, что ничего вдруг не сломается. Потому что рефлексия уже предполагает зависимость от данных в рантайме, а в жизни бывает всякое.


        1. Devoter Автор
          07.12.2022 23:39

          И снова, здравствуйте. )) Мне кажется, подобную мысль в комментариях придется еще несколько раз повторить. Цель текста: обозначить имеющиеся подводные камни, на которые стоит обратить внимание, каким способом проблему решать - каждый выбирает сам для себя. Перебор ключей не используется для решения, я с его помощью лишь продемонстрировал тот факт, что наличие или отсутствие undefined-поля можно определить лишь по наличию ключа, но не по значению.Что касается рефлексии, то я тоже сторонник жесткого описания и проверки схемы, причем, как на стороне фронта, так и на стороне бекенда (от нарушения контрактов никто не застрахован). Возможно, в текст стоило включить упоминание того, что в старых фреймворках (без использования Proxy для реактивных свойств) невозможно отследить изменение свойства, если оно отсутствует в объекте (тот же Vue 2.x).


        1. Ilusha
          07.12.2022 23:51

          Я Вам привел в пример, когда это абсолютно разные вещи. Тот же hasOwnProperty будет возвращать разное значение. И есть еще эффекты, мне просто лень набирать доказательную базу. В частности, не просто так существует оператор "delete"

          Когда итерация по ключам стала "хаком" или "проблемой"? И почему?
          Кмк, отличная история, особенно, когда у нас есть декларативные описания, а порядок не важен для структуры. Или когда есть динамические структуры. Если я не прав, дайте референсы, проведу себе ликбез)

          Да, есть зависимость от данных в райнтайме. Только мы всегда от них зависим.
          Упрощенно, банальный конструктор форм. Нет поля: нет формы для него. Есть поле: показываем форму.


  1. miscusi
    07.12.2022 22:58

    включаешь строгий режим в TS, перестаешь сравнивать с приведением типа (кроме единственного случая когда null == undefined)

    и проблема пропадает почти полностью


    1. Devoter Автор
      07.12.2022 22:59

      Во-первых, про TS сказано почти в самом начале, а во-вторых, в том-то и дело, что "почти". И цель не показать - какие классные решения есть, а указать на нюанс языка, который нужно знать, и который имеет место быть.


  1. somedevfox
    07.12.2022 22:58

    Могу согласится, что undefined - не нужная часть языка, но бросать исключение не имеет смысла, потому-что ECMAScript - динамический язык.

    В динамическом языке, вы не знаете - с чем работаете, что не правда в статических, как C и Rust. Даже если null и undefined - одно и тоже (исключая сравнения ===), это не отменяет концепт неинициализированных переменных.

    Не имеет смысла бросать исключения на тип, который вы не можете знать.


    1. Devoter Автор
      07.12.2022 23:06

      Python с вами не согласен

      d: int
      print(d)

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


      1. Devoter Автор
        07.12.2022 23:30
        -1

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

        d = { 'id': 1 }
        print(d['id']) // 1
        print(d.get('desc', None)]) // None
        print(d['desc']) // KeyError


  1. Ilusha
    07.12.2022 23:10
    +4

    Это не JSON.stringify фильтрует undefined, это стандарт JSON не включает в себя такой тип данных. Подозреваю, потому что на другой стороне его может не быть.


    1. Devoter Автор
      07.12.2022 23:16
      -2

      Вы сами придумали этот вывод? В тексте так и написано, что типа undefined нет в JSON. Зачем писать комментарий, если невнимательно прочитали?


      1. Ilusha
        07.12.2022 23:37
        +1

        Да, мой косяк. В куче воды не увидел вывода.

        У Вас пол статьи посвящено "пропадет поле с undefined" с расписыванием примера сериализации и дессериализации. Зачем так писать?)

        Вот, я сократил статью вдвое:
        const obj = { id: 1, name: null, desc: undefined };
        JSON.stringify(obj) === '{"id":1,"name":null}' // true


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

        А Ваш вывод

        Тип Undefined явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную

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


        1. Devoter Автор
          07.12.2022 23:44
          -1

          Тут все просто же. Если вы прочитали дисклеймер, то стоило просто прекратить чтение. Расписано подробно как раз для тех, кто нюанса не знает, а вот сравнение объектов через JSON.stringify - очень плохая идея:

          const a = { id: 1, name: null };
          const b = { name: null, id: 1 };
          
          console.log(JSON.stringify(a));
          console.log(JSON.stringify(b));


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


          1. Ilusha
            08.12.2022 00:03

            Вы меня простите, но я все-таки сам решу, что читать, а что - нет.

            Если бы Ваш вывод не имел продолжения, которое после первой запятой, то я бы согласился. Null был бы предпочитительнее, кмк.
            Но то, как "должно работать", не укладывается в парадигму js с позиции типизации.


            1. Devoter Автор
              08.12.2022 00:09
              -1

              Почему не укладывается? Можно подробней?


              1. Ilusha
                08.12.2022 00:21

                Потому что слабая типизация. Рафинированный пример
                const t
                export const test = (param: number) => {
                if (!t) { // это обращение к переменной, где Вы хотели бы выкинуть исключение
                t = param
                }
                return t * param
                }
                Да, можно было бы придумать функцию проверки, аналогичную isNaN.


                1. Devoter Автор
                  08.12.2022 00:32
                  -1

                  Поздравляю, вы только что запретили использовать 0 в качестве константы t. Слабая типизация к вашему утверждению не имеет никакого отношения, она имеет отношение к неявному приведению типов, как пример - обычное ==, когда 1 == '1' возвращает true. Обращение к неинициализированной переменной либо является ошибкой (бросается исключение), либо является неопределенным поведением (undefined - как раз вариант оного). Чем меньше UB, тем проще писать надежные программы.


                  1. Ilusha
                    08.12.2022 01:43

                    Пример был рафинированный для иллюстрации. И это явно указал.
                    Но, ок, модифицируем, но все равно оставим рафинированным:

                    const calc = (val1: unknown, val2: unknown):unknown => {...}
                    const previous:unknown
                    export const getCalcValue = (value: unknown):unknown => {
                       if (previous === undefined) {
                           previous = value
                           return value
                       }
                    
                       return calc(previous, value)
                    }


                    Вот еще один рафинированный.

                    class A {}
                    const insts:Record<string, A> = {}
                    export default function getInstByName(name: string):A {
                      if (!insts[name]) {
                        insts[name] = new A()
                      }
                      return insts[name]
                    


                    Я спорю с этим утвержением:

                    логичней было бы выбрасывать исключение

                    В данным примере не логичнее. Но я специально его не брал, потому что здесь можно hasOwnProperty использовать. И вот сюда можно слабую типизацию подвести: нам нужны тайпчекеры для определения типа(а где их невозможно применить по факту: type assertions).

                    Чем меньше UB, тем проще писать надежные программы.

                    Согласен. Но исключение на обращение к undefined никак не решает проблему UB. Ничего не изменится, если там будет null, который тоже про UB.


                    1. Devoter Автор
                      08.12.2022 03:05
                      -1

                      Вы натягиваете сову на глобус, но это, хотя бы, весело.

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

                      const calc = (val1: unknown, val2: unknown):unknown => {...}
                      let hasPrevious = false
                      let previous: unknown
                      export const getCalcValue = (value: unknown):unknown => {
                         if (!hasPrevious) {
                             hasPrevious = true
                             previous = value
                             return value
                         }
                      
                         return calc(previous, value)
                      }

                      Правда, в вашем примере меня смущает то, что previous - константа, очевидно, что ей нельзя задать присвоить значение в строке 7. Да, у нас добавилась переменная, но этот мелкий недостаток можно пережить, зато никакого UB. Также странно для меня выглядит использование типа unknown для calc, ведь его семантика в том, что мы нам не важно - что это за значение, мы просто передаем его куда-то дальше или возвращаем, но к рассматриваемому вопросу это не имеет никакого отношения.

                      Про второй пример вы сами все сказали, нужно сперва проверять наличие ключа в словаре, а потом уже получать значение свойства, либо делать это так (хотя проверка поля в данном случае предпочтительней):

                      class A {}
                      const insts:Record<string, A> = {}
                      export default function getInstByName(name: string):A {
                        let instance;
                        
                        try {
                          instance = insts[name];
                        } catch (e) {
                          instance = new A();
                          insts[name] = instance;
                        }
                      
                        return instance;
                      }

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


          1. GothicJS
            08.12.2022 00:07

            Мой вывод основан на знании других языков программирования, которые прекрасно обходятся без этого типа.

            Ну так и без прототипного наследования обходятся, и что теперь, отменить?) JS вообще в некотором смысле язык особенный, это нужно понять и принять)


            1. Devoter Автор
              08.12.2022 00:14

              Все верно, поэтому так и написал: "Нам остается лишь помнить об этих особенностях при написании кода". Утверждение о том, что было бы лучше иначе, не противоречит тому, что есть так, как есть, и надо уметь с этим жить.


  1. funca
    08.12.2022 00:02
    +2

    Тип Undefined явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную, но Брендан Айк пошел по другому пути, впрочем, винить его за это сложно

    В первых версиях JS не было поддержки исключений и undefined в результате по замыслу был равносилен ошибке. Если в ваших вычислениях или данных появился undefined, то все дальнейшие манипуляции это UB. Поэтому иметь в результатах undefined считалось плохой практикой (хотя на практике с этим можно жить, до поры до времени, - ваши примеры с JSON и lodash в этом смысле показательны).

    В дальнейшем это было несколько раз переосмыслено. Был период, когда void(0) в коде вызывал неодумение. Потом стал паттерном в порядке вещей. Потом придумали разные optional (arguments, chaining и т.п.), сделав вычисления с undefined вполне легитимными. В результате имеем то, что имеем.


    1. Devoter Автор
      08.12.2022 00:09
      -1

      Как-то упустил из виду, что исключения в JS были не сразу.