Comments 92
Статья для кто работает с JavaScript без документации?
Вы прочитали всю актуальную спецификацию ECMAScript? Но, в целом, да, и ремарка насчет игнорирования рекомендаций комитета на это намекает. Реальность не так идеальна, как нам хотелось бы.
Я бы хотела вас поддержать. Документация документацией, но она толстая. Мало кто всё от корки до корки прочёл.
Я обожаю копаться в доке (и не только в ней), но никогда не сталкивалась с десериализацией данных с исходными undefined. Вернее, со сравнением объектов после сериализации-десериализации при таких данных.
Словила инсайт. Спасибо:)
но никогда не сталкивалась с десериализацией данных с исходными
undefined. Вернее, со сравнением объектов после
сериализации-десериализации при таких данных.
А вы для себя подумайте - почему вы с таким не сталкивались.
А как же сериализация, когда вы отправляете данные на бэк?)
null - поле отправится, undefined - будет отфильтровано.
помощи
lodash.isEqual
сравним копию и десериализованный результат с оригиналом:
Дак это же проблема какой-то библиотеки.
undefined не хуже и не лучше null, оба ИМХО не нужны в современной ЯП, но что поделать.
Насчет первого - нет, это не проблема библиотеки.
Насчет второго - поясните, позволю с вами не согласиться. null
нужен, иначе как вы укажете на явное отсутствие значения, например, отсутствие объекта в переменной, который может там быть, а может и не быть.
Вообще то null, это тоже значение
Смотря что вы называете значением. В типизированных языках (с++), ссылочные типы (точнее переменные/объекты) хранят ссылку на область памяти где находятся данные, которые и есть значение. Но null представляет собой отсутствие такой ссылки на область памяти и никуда не ссылается и в упомянутом смысле не является значением. Это отсутствие ссылки на значение или ссылка в никуда.
Насчет второго - поясните, позволю с вами не согласиться.
null
нужен, иначе как вы укажете на явное отсутствие значения, например, отсутствие объекта в переменной, который может там быть, а может и не быть.
Для этого во многих языках существует Optional / Maybe где ты можешь явно на уровне типов разрулить, возможно ли отсутствие данных.
Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.
Естественно, это больше актуально для статически-типизированных языков.
Не силен в функциональных языках, так что спрошу: если поле/переменная Optional, то каково будет ее значение, если там нет ничего? Разве не тот же null
?
Нет, будет None.
Там же нет null ;)
Это шутка такая? Чем None
отличается от null
? В C++ есть nullptr
, к примеру, а в Go - nil
.
Тем, что мы явно это обрабатываем.
Не возникает ситуаций, когда что-то где-то вернуло null, а мы забыли проверку и всё пошло по одному месту. Если не используем Optional - точно знаем, что None не будет, если используем - то обрабатываем варианты: когда None и когда Some.
Это называется Sound null safety и никакого отношения к моему комментарию не имеет. Речь о том, что значение null/nil/none все равно есть. Изначально я в ветке оспаривал это утверждение:
undefined не хуже и не лучше null, оба ИМХО не нужны в современной ЯП, но что поделать.
Подсказка: typeof NaN === 'number'
Поясните, что хотели сказать этим комментарием? Ведь typeof null на самом деле не объект.
Разница в том, что с None ты можешь сделать любое количество уровней вложенности
Например Some(Some(Some(None))), а с null так нельзя.
Плюс нулябельность показана явно.
Плюс None - это значение вполне конкретного типа Option<T>, а не значение само по себе.
Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.
У автора вылезла проблема при сериализации структуры данных в JSON. Как вы посоветуете представлять в этом формате Optional? А для ситуации когда они вложены друг в друга?
Покажите как будет JSON выглядеть с Optional полем. А ещё с полем, тип которого Optional, вложенный в другой Optional
Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.
После того, как вы null в null вложите
Вы ж сами хвастались этим. На что разумно сказали, что у автора подгорело от сериализации, так что вернули разговор в русло json. Так что ветку читайте внимательнее
Пустой Optional обычно сериализуется как null
в JSON, потому что зачем нужен тип Optional<Optional<Otional<...>>>
или Maybe (Maybe (Maybe (... t)))
?
В разных языках по-разному принято. В Java и Scala используются Optional<T> / Option[T]. В Kotlin есть nullable types (как и в TypeScript), там принято использовать их вместо Optional.
null
нужен, иначе как вы укажете на явное отсутствие
значения, например, отсутствие объекта в переменной, который может там
быть, а может и не быть
Для рантайма option, для сериализации с непостоянной структурой просто не будет поля, с постоянной добавляется второе поле XXXisNull: true/false либо на всю структуру битовая маска какие поля заполнены.
Опять же это не для всех ЯП. В тех в которые вотнули null уже ничего не поделаешь, везде будут проверки на != Null
А как работает option без значения null? Должен быть тогда какой-то оператор который возвращает true или false в зависимости от наличия/отсутствия значения, а также какая-то встроенная функция или оператор, которая позволяет выставить поле в неопределенное. Тогда это тоже самое, только без явного наличия значения.
В том что Optional это отдельный тип. Ничего кроме Optional не может быть None, нельзя взять, например, вектор и передать его аргументом в функцию как None. Если же функция подразумевает, что значение аргумента может отсутствовать, то тип параметра будет Optional<Vec<...>>, и чтобы получить значение с Optional тебе надо использовать паттерн матчинг или один из миллиона удобных методов, например unwrap_or_default(). Тут надо специально постараться, чтобы не заметить отличие от обычного null, коим может являться все что угодно.
Понятно, но, думаю, вы все-таки путаете null safety и отсутствие null как такового. То, что вы описали, примерно то, что я представлял. Если не учитывать различные небезопасные преобразования, то тот же TypeScript также не позволит записать null в поле конкретным типом, как и undefined. Но если TS - надстройка, то тот же Dart вполне четко не позволяет использовать null для переменных и полей, которым явно этот тип не разрешен. Так что, принципиальной разницы нет. Другое дело, что динамические языки с null (тот же JS) позволяют его пихать куда угодно, что, безусловно, усложняет ситуацию.
Мне кажется вы путаете null и отсуствие значения. т.к. null это значение, а Optional -> None это тип, который указывает на отсутствие значения.
Просто в языках с динамической типизацией, к коим относится и JS, типы связаны со значениями в рантайме и по-другому не бывает.
В теории, если разделять понятия Model of computation и Execution model, то можно говорить о корректности программы - с точки зрения алгоритма (где для проверки на помощь приходит логика, в т.ч. автоматика в виде статического код анализа и тайп чекеров), и с точки зрения рантайма - как о связанных, но не тождественных понятиях. В этом смысле система типов в TS и особенности исполнения JS вещи ортогональные.
Так что вводные бла-бла-бла оставим за скобками и рассмотрим основные подводные камни, которые бросает нам под ноги
undefined
.
Просто ради интереса вопрос к автору: из какого языка вы дошли до JS ?
Теперь создадим объект, соответствующий интерфейсу выше:
const obj = { id: 1, name: null, desc: undefined };
А какой логикой вы руководствуетесь, когда ставите в качестве значения undefined ? Почему одно свойства null, а другое undefined ?
Потому что блин оно само undefined, когда переменная создана, но ничего не вписано. С какого причём языка сюда не приди.
(попробую пофантазировать) У вас где-то есть отдельные id, name и desc. Вы обустраиваете их в объект через {id, name, desc}. Если desc задана, но не инициализирована, она попадёт в объект как undefined.
Если desc задана, но не инициализирована, она попадёт в объект как undefined.
А кто виноват в том, что она задана, но не инициализирована?
Вы говорите о причинах, в тексте написано - как такой ситуации избежать (используйте null
), но рассматривается вариант, который реально возможен и встречается в проектах. Оценки плохо/хорошо тут нет. Это может быть сторонняя библиотека, автор которой посчитал, что использование undefined
вместо null
- отличная идея. Ситуация может возникнуть в результате разных обстоятельств: функция, которая должна вернуть объект - вернула undefined,
а мы записываем это значение в поле нашего объекта, и да, в таком случае, правильно было бы обработать этот результат, скажем так:
const prepared = {
internal: getAnotherObject() ?? null
};
Собственно, цель статьи была обратить внимание на имеющиеся подводные камни. А ваша претензия звучит как "хорошо делай - хорошо будет". Да кто ж с этим спорит-то?
Не-не-не, давно уже есть языки, которые за использование неинициализированных переменных дадут вам по рукам...
Так что "с какого языка ни приди" – неточно.
Есть языки, где не используются изменяемые переменные, либо все переменные инициализируются, там в принципе не возникает такого вопроса.
А в языках с переменными, ссылочными типами и null, объект обычно инициализируется как null, и нет никакого undefined.
Так что это скорее "с какого языка не приди", лишь бы он был JavaScript или TypeScript.
Мне вообще ничего из этого боль не причиняет, но не раз встречал подобные проблемы у других на каком-либо этапе. Давайте будем честны, с дуру можно сделать много чего, но вероятность того, что разработчик по незнанию попытается сериализовать какой-нибудь Symbol сильно меньше, чем это может произойти с undefined-значением.
С точки зрения интерпретатора что desc?: string;
, что desc: string | undefined
равносильны, если подскажете способ различить эти определения, кроме как визуально - буду рад. Что касается различных схем, сериализаторов и так далее - ясно дело, что решения есть, правда, чем именно перегрузка toJSON может вам помочь мне неведомо, если вы имеете ввиду, что можно сериализовать undefined-поле как null, то это как раз далеко не то, что нужно, если конктракт подразумевает наличие опционального, а не nullable-поля.
С точки зрения интерпретатора что
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
И что доказывает ваш пример? Что если объявить два разных объекта по-разному, что будет разный результат? Что мешает написать const example2 = { a: 1, b: undefined } as { a: number; b?: string }
? К тому же, вы вновь аппелируете к TypeScript, когда речь в статье о JS.
Upd: извиняюсь, думал, что отвечаю предыдущему комментатору.
Вы задали вопрос:
если подскажете способ различить эти определения, кроме как визуально - буду рад.
Я на него ответил. Как с позиции TS, так и с позиции JS.
С точки зрения TS - только в одну сторону, а с точки зрения JS - я этот вариант (с проверкой наличия ключа) как единственный способ проверить и привел в тексте публикации, но, хорошо, аргумент принят. Статью поправлю.
Upd: ради интереса проверил, если использовать as, то TS не ругается:
const example2 = { a: 1 } as { a: number; b: string | undefined };
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 значениями.
Второй способ: итерирование по ключам. И здесь это тоже важно.
Мой комментарий ниже, вроде как, дополняет предыдущий. Через объявление типа через двоеточие да, будет ругаться. Если же привести к типу через as - не будет, так что даже тут TS не спасет в 100% случаев, не говоря уже о том, что только что десериализованный объект имеет тип any, который приводится к чему угодно, так что дополнительных проверок не избежать. Такое чувство, что вы пытаетесь найти огрехи в коде, который лишь является упрощенной иллюстрацией возможного развития событий. Думаю, если бы я описал еще более подробно, вы бы сказали, что воды столько, что читать вообще невозможно. Что касается stringify+parse, то undefined-поля будут просто удалены, про Vue 2 я имел ввиду всякие Vuex или computed. computed просто не заметит появление отсутствовашего свойства в объекте, даже если он отслеживаемый.
такое чувство, что вы пытаетесь найти огрехи в коде
Да. Потому что Ваша статья будет проиндексирована и люди будут натыкаться на неё. Мы же не на кухне и не в ламповом чатике)
Я может где-то груб и душнила, за это простите, но без негатива. Для меня хабр с комментами - это в т.ч. в некотором роде образовательная среда.
Type Assertions в ts, имхо, недостаточно строг. Но, возможно, это баланс между функциональностью и строгостью.
Но валидность выражения const t = {} as string
лично у меня вызывает вопросы.
Дело не в подробности, а в лаконичной точности с разумным раскрытием смысла. "Вода" - это обозначение того, что иллюстрация проблемы и отход в стороны размазывает суть. Это как насытить предложение множеством разных оборотов.
...
Никогда на слух не мог воспринять это предложение. Да и при чтении тоже приходится напрягаться.
В белом плаще с кровавым подбоем, шаркающей кавалерийской походкой, ранним утром четырнадцатого числа весеннего месяца нисана в крытую колоннаду между двумя крыльями дворца Ирода Великого вышел прокуратор Иудеи Понтий Пилат.
Ну, к слову сказать, вашими и не только стараниями, кое-какие правки и пояснения внес, за что спасибо, конечно. В данном случае вода необходимо, чтобы было понятно пошагово - что же происходит. Просто на примерах не все могут уловить смысл. Вы читаете с позиции того, кто уже знает, как оно работает, и для вас это - вода. А для тех, кто не знает - должно дать понимание ситуации: возможные причины ее возникновения, последствия и варианты обхода/решения.
Если хотели сказать, что при описании типа как {a: number; b: string | undefined }
нельзя задать переменной значение { a: 1 }
, то стоило так и написать. И все равно, мы упираемся в TypeScript, о котором речи не идет.
По поводу контракта с обязательным полем тут уже в комментариях поговорили, и текст я поправил, действительно, правильней описать именно как опциональное поле. Сам контракт был приведен просто чтобы описать ожидаемую структуру объекта. Причем тут ваши высказывания о том, что объекты это не просто структуры (C++ вот начал очень сильно возмущаться) мне неведомо. Понятное дело, что есть не только JSON, но любые другие сериализаторы с поддержкой опциональных полей могут вести себя ровно так же, тут, как вы верно заметили, нужна валидация. И вариантов тут два: используется готовая библиотека для валидации (хотя кто-то же ее ранее написал), валидация пишется вручную, или если бы было написано "валидируйте входящие данные", то вопроса бы у вас не возникло? Вот в случае написания проверок вручную и стоит знать о такой особенности поведения. И да, это именно нюанс языка, из которого такое поведение становится возможным. Какие возможности дает наличие undefined, мне, правда, неясно.
Upd: Ах, погодите, в тексте же есть указание на то, что потребуются дополнительные проверки. Или это не валидация?
Внес в текст некоторые пояснения, в том числе - упоминание валидации. Надеюсь, это улучшит текст.
по-хорошему Parse, don’t validate.
не знал что парсер JSON выкидывает поля с undefined
, класная фича же, оказывается от него даже польза есть.
что касается примеров то я честно говоря не очень понял в чем конкретная претензия к undefined
. Он и есть как бы "неопределенный тип" js-рантайм понятия не иммеет какой тип будет у let a;
, а null
- это тип object
... Тип
Undefined
явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную ...
Лично я считаю, что Undefined, в некоторых случаях, крайне полезная штука при отладке кода. :)
Ну, потеряли при сериализации мы поле и что? Вы же сами написали в интерфейсе, что поле имеет тип string | undefined
. Значит, прежде чем его использовать в качестве строки, надо проверить на undefined
field: string | undefined подразумевает существование поля. field?: string подразумевает, что поле потенциально отсутствует. Это разные контракты.
Практическое значение это имеет, например, в том случае, когда мы хотим итерироваться по ключам объекта.
В рамках JS это одно и тоже, хотя семантически это разные вещи и соглашусь с вами. Но это не отменяет проверок на undefined.
И если уж честно говорить, то итерация по ключам это слишком грязный хак для решения какой-либо проблемы. Если нет поля, значит и нет значения, тем более раз по интерфейсу так предполагается. Может на фронте часто такие задачи, но на беке редко приходится "рефлексировать".
Рефлексия очень заманчива, но не стоит ею увлекаться. И если использовать, то только тогда, когда есть уверенность на 100% в результатах, что ничего вдруг не сломается. Потому что рефлексия уже предполагает зависимость от данных в рантайме, а в жизни бывает всякое.
И снова, здравствуйте. )) Мне кажется, подобную мысль в комментариях придется еще несколько раз повторить. Цель текста: обозначить имеющиеся подводные камни, на которые стоит обратить внимание, каким способом проблему решать - каждый выбирает сам для себя. Перебор ключей не используется для решения, я с его помощью лишь продемонстрировал тот факт, что наличие или отсутствие undefined-поля можно определить лишь по наличию ключа, но не по значению.Что касается рефлексии, то я тоже сторонник жесткого описания и проверки схемы, причем, как на стороне фронта, так и на стороне бекенда (от нарушения контрактов никто не застрахован). Возможно, в текст стоило включить упоминание того, что в старых фреймворках (без использования Proxy для реактивных свойств) невозможно отследить изменение свойства, если оно отсутствует в объекте (тот же Vue 2.x).
Я Вам привел в пример, когда это абсолютно разные вещи. Тот же hasOwnProperty будет возвращать разное значение. И есть еще эффекты, мне просто лень набирать доказательную базу. В частности, не просто так существует оператор "delete"
Когда итерация по ключам стала "хаком" или "проблемой"? И почему?
Кмк, отличная история, особенно, когда у нас есть декларативные описания, а порядок не важен для структуры. Или когда есть динамические структуры. Если я не прав, дайте референсы, проведу себе ликбез)
Да, есть зависимость от данных в райнтайме. Только мы всегда от них зависим.
Упрощенно, банальный конструктор форм. Нет поля: нет формы для него. Есть поле: показываем форму.
включаешь строгий режим в TS, перестаешь сравнивать с приведением типа (кроме единственного случая когда null == undefined)
и проблема пропадает почти полностью
Могу согласится, что undefined
- не нужная часть языка, но бросать исключение не имеет смысла, потому-что ECMAScript - динамический язык.
В динамическом языке, вы не знаете - с чем работаете, что не правда в статических, как C
и Rust
. Даже если null
и undefined
- одно и тоже (исключая сравнения ===
), это не отменяет концепт неинициализированных переменных.
Не имеет смысла бросать исключения на тип, который вы не можете знать.
Python с вами не согласен
d: int
print(d)
От того, что тип переменной динамический, не значит, что такая переменная не существует, это не одно и то же. Не знаю, о каком исключении на тип идет речь, я лишь говорю о том, что при попытке почитать неинициализированную переменную или несуществующее поле, гораздо логичней выбрасывать исключение. В случае с переменной это позволило бы автоматически снизить большое количество обидных ошибок, а для полей объектов есть способы проверки наличия поля.
Это не JSON.stringify фильтрует undefined, это стандарт JSON не включает в себя такой тип данных. Подозреваю, потому что на другой стороне его может не быть.
Вы сами придумали этот вывод? В тексте так и написано, что типа undefined
нет в JSON. Зачем писать комментарий, если невнимательно прочитали?
Да, мой косяк. В куче воды не увидел вывода.
У Вас пол статьи посвящено "пропадет поле с undefined" с расписыванием примера сериализации и дессериализации. Зачем так писать?)
Вот, я сократил статью вдвое:const obj = { id: 1, name: null, desc: undefined };
JSON.stringify(obj) === '{"id":1,"name":null}' // true
И это я не говорю про ошибку эквивалентности интерфейсов (на которую будет ts ругаться). Вырежем эту ошибочную воду и статья станет размером с коммент.
А Ваш вывод
Тип
Undefined
явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную
основывается на непонимании того, что undefined это как значение поля, так и отсутствие этого поля, когда мы говорим про объект.
Тут все просто же. Если вы прочитали дисклеймер, то стоило просто прекратить чтение. Расписано подробно как раз для тех, кто нюанса не знает, а вот сравнение объектов через JSON.stringify
- очень плохая идея:
const a = { id: 1, name: null };
const b = { name: null, id: 1 };
console.log(JSON.stringify(a));
console.log(JSON.stringify(b));
Не делайте выводов о моем понимании или не понимании, лучше спросите явно. Мой вывод основан на знании других языков программирования, которые прекрасно обходятся без этого типа.
Вы меня простите, но я все-таки сам решу, что читать, а что - нет.
Если бы Ваш вывод не имел продолжения, которое после первой запятой, то я бы согласился. Null был бы предпочитительнее, кмк.
Но то, как "должно работать", не укладывается в парадигму js с позиции типизации.
Почему не укладывается? Можно подробней?
Потому что слабая типизация. Рафинированный примерconst t
Да, можно было бы придумать функцию проверки, аналогичную isNaN.
export const test = (param: number) => {
if (!t) { // это обращение к переменной, где Вы хотели бы выкинуть исключение
t = param
}
return t * param
}
Поздравляю, вы только что запретили использовать 0
в качестве константы t
. Слабая типизация к вашему утверждению не имеет никакого отношения, она имеет отношение к неявному приведению типов, как пример - обычное ==
, когда 1 == '1'
возвращает true
. Обращение к неинициализированной переменной либо является ошибкой (бросается исключение), либо является неопределенным поведением (undefined - как раз вариант оного). Чем меньше UB, тем проще писать надежные программы.
Пример был рафинированный для иллюстрации. И это явно указал.
Но, ок, модифицируем, но все равно оставим рафинированным:
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.
Вы натягиваете сову на глобус, но это, хотя бы, весело.
Первый пример: тут вы именно используете фишку наличие типа 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 базируется на том, пользы от него практически никакой, лишь кое-где позволяет сократить количество кода, зато проблем неопределенное поведение доставляет куда как больше.
Мой вывод основан на знании других языков программирования, которые прекрасно обходятся без этого типа.
Ну так и без прототипного наследования обходятся, и что теперь, отменить?) JS вообще в некотором смысле язык особенный, это нужно понять и принять)
Тип Undefined явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную, но Брендан Айк пошел по другому пути, впрочем, винить его за это сложно
В первых версиях JS не было поддержки исключений и undefined в результате по замыслу был равносилен ошибке. Если в ваших вычислениях или данных появился undefined, то все дальнейшие манипуляции это UB. Поэтому иметь в результатах undefined считалось плохой практикой (хотя на практике с этим можно жить, до поры до времени, - ваши примеры с JSON и lodash в этом смысле показательны).
В дальнейшем это было несколько раз переосмыслено. Был период, когда void(0) в коде вызывал неодумение. Потом стал паттерном в порядке вещей. Потом придумали разные optional (arguments, chaining и т.п.), сделав вычисления с undefined вполне легитимными. В результате имеем то, что имеем.
Внедрение типа Undefined в JavaScript, на мой взгляд, было ошибкой с далеко идущими последствиями.Прочитав это, специально проверил, не Егор ли Бугаенко написал статью. Не он, но дело его живёт)
Вся боль undefined