Обучающих статей, рассматривающих различия между 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)
shai_hulud
07.12.2022 19:51-10помощи
lodash.isEqual
сравним копию и десериализованный результат с оригиналом:Дак это же проблема какой-то библиотеки.
undefined не хуже и не лучше null, оба ИМХО не нужны в современной ЯП, но что поделать.
Devoter Автор
07.12.2022 20:08Насчет первого - нет, это не проблема библиотеки.
Насчет второго - поясните, позволю с вами не согласиться.
null
нужен, иначе как вы укажете на явное отсутствие значения, например, отсутствие объекта в переменной, который может там быть, а может и не быть.rsashka
07.12.2022 20:51+8Вообще то null, это тоже значение
mozg3000tm
08.12.2022 23:38Смотря что вы называете значением. В типизированных языках (с++), ссылочные типы (точнее переменные/объекты) хранят ссылку на область памяти где находятся данные, которые и есть значение. Но null представляет собой отсутствие такой ссылки на область памяти и никуда не ссылается и в упомянутом смысле не является значением. Это отсутствие ссылки на значение или ссылка в никуда.
vabka
07.12.2022 22:12+3Насчет второго - поясните, позволю с вами не согласиться.
null
нужен, иначе как вы укажете на явное отсутствие значения, например, отсутствие объекта в переменной, который может там быть, а может и не быть.Для этого во многих языках существует Optional / Maybe где ты можешь явно на уровне типов разрулить, возможно ли отсутствие данных.
Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.
Естественно, это больше актуально для статически-типизированных языков.
Devoter Автор
07.12.2022 22:36-1Не силен в функциональных языках, так что спрошу: если поле/переменная Optional, то каково будет ее значение, если там нет ничего? Разве не тот же
null
?vabka
07.12.2022 23:11+1Нет, будет None.
Там же нет null ;)
Devoter Автор
07.12.2022 23:14Это шутка такая? Чем
None
отличается отnull
? В C++ естьnullptr
, к примеру, а в Go -nil
.TheTryProgrammerName
07.12.2022 23:44+2Тем, что мы явно это обрабатываем.
Не возникает ситуаций, когда что-то где-то вернуло null, а мы забыли проверку и всё пошло по одному месту. Если не используем Optional - точно знаем, что None не будет, если используем - то обрабатываем варианты: когда None и когда Some.Devoter Автор
07.12.2022 23:49+1Это называется Sound null safety и никакого отношения к моему комментарию не имеет. Речь о том, что значение null/nil/none все равно есть. Изначально я в ветке оспаривал это утверждение:
undefined не хуже и не лучше null, оба ИМХО не нужны в современной ЯП, но что поделать.
0xd34df00d
08.12.2022 06:08+5null принадлежит любому типу (насколько это высказывание вообще имеет смысл для js), nullptr принадлежит любому типу-указателю. Nothing принадлежит только Maybe.
nin-jin
08.12.2022 06:49+20xd34df00d
08.12.2022 08:53+3Очень хорошо. Давайте теперь вернёмся к исходному тезису о разнице между
null
/nullptr
иNothing
.
funca
07.12.2022 23:17+4Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.
У автора вылезла проблема при сериализации структуры данных в JSON. Как вы посоветуете представлять в этом формате Optional? А для ситуации когда они вложены друг в друга?
0xd34df00d
08.12.2022 06:09+1Брать и представлять, вроде Maybe String и так далее. В чем конкретно ваш вопрос?
dopusteam
08.12.2022 07:34+1Покажите как будет JSON выглядеть с Optional полем. А ещё с полем, тип которого Optional, вложенный в другой Optional
0xd34df00d
08.12.2022 08:11+1Покажите как будет JSON выглядеть с Optional полем.
Зависит от настроек сериализатора. Могу сделать так, что первый же отсутствующий Maybe будет, ээ, отсутствовать и в JSON.
А ещё с полем, тип которого Optional, вложенный в другой Optional
После того, как вы null в null вложите.
dopusteam
08.12.2022 08:44Причём Optional позволяет описывать гораздо более сложные ситуации, чем null, благодаря тому что его можно сколько угодно раз друг в друга вкладывать.
После того, как вы null в null вложите
Вы ж сами хвастались этим. На что разумно сказали, что у автора подгорело от сериализации, так что вернули разговор в русло json. Так что ветку читайте внимательнее
0xd34df00d
08.12.2022 08:52+2Для разных юзкейсов я буду выбирать разные форматы. Если мне нужно представлять
Maybe (Maybe Ty)
, то одним полем не обойтись (но тогда и у исходного автора проблемы), но если не нужно, то я могу оставить совместимость с предложенной схемой и иметь на своей стороне нормальныйMaybe
. Всё ещё не понимаю контр-аргумент, короче.funca
08.12.2022 13:14+1Вообще вопрос "покажите .." подразумевает ответ в виде именно показывания конечного результата, а не объяснение как бы вы это делали. ;)
shai_hulud
08.12.2022 02:32null
нужен, иначе как вы укажете на явное отсутствие
значения, например, отсутствие объекта в переменной, который может там
быть, а может и не бытьДля рантайма option, для сериализации с непостоянной структурой просто не будет поля, с постоянной добавляется второе поле XXXisNull: true/false либо на всю структуру битовая маска какие поля заполнены.
Опять же это не для всех ЯП. В тех в которые вотнули null уже ничего не поделаешь, везде будут проверки на != Null
Devoter Автор
08.12.2022 03:08А как работает option без значения null? Должен быть тогда какой-то оператор который возвращает true или false в зависимости от наличия/отсутствия значения, а также какая-то встроенная функция или оператор, которая позволяет выставить поле в неопределенное. Тогда это тоже самое, только без явного наличия значения.
HelpOrMe
08.12.2022 04:35В том что Optional это отдельный тип. Ничего кроме Optional не может быть None, нельзя взять, например, вектор и передать его аргументом в функцию как None. Если же функция подразумевает, что значение аргумента может отсутствовать, то тип параметра будет Optional<Vec<...>>, и чтобы получить значение с Optional тебе надо использовать паттерн матчинг или один из миллиона удобных методов, например unwrap_or_default(). Тут надо специально постараться, чтобы не заметить отличие от обычного null, коим может являться все что угодно.
Devoter Автор
08.12.2022 04:44Понятно, но, думаю, вы все-таки путаете null safety и отсутствие null как такового. То, что вы описали, примерно то, что я представлял. Если не учитывать различные небезопасные преобразования, то тот же TypeScript также не позволит записать null в поле конкретным типом, как и undefined. Но если TS - надстройка, то тот же Dart вполне четко не позволяет использовать null для переменных и полей, которым явно этот тип не разрешен. Так что, принципиальной разницы нет. Другое дело, что динамические языки с null (тот же JS) позволяют его пихать куда угодно, что, безусловно, усложняет ситуацию.
shai_hulud
08.12.2022 12:34+1Мне кажется вы путаете null и отсуствие значения. т.к. null это значение, а Optional -> None это тип, который указывает на отсутствие значения.
funca
08.12.2022 13:03Просто в языках с динамической типизацией, к коим относится и JS, типы связаны со значениями в рантайме и по-другому не бывает.
В теории, если разделять понятия Model of computation и Execution model, то можно говорить о корректности программы - с точки зрения алгоритма (где для проверки на помощь приходит логика, в т.ч. автоматика в виде статического код анализа и тайп чекеров), и с точки зрения рантайма - как о связанных, но не тождественных понятиях. В этом смысле система типов в TS и особенности исполнения JS вещи ортогональные.
GothicJS
07.12.2022 21:13+3Так что вводные бла-бла-бла оставим за скобками и рассмотрим основные подводные камни, которые бросает нам под ноги
undefined
.Просто ради интереса вопрос к автору: из какого языка вы дошли до JS ?
Теперь создадим объект, соответствующий интерфейсу выше:
const obj = { id: 1, name: null, desc: undefined };
А какой логикой вы руководствуетесь, когда ставите в качестве значения undefined ? Почему одно свойства null, а другое undefined ?MaryRabinovich
07.12.2022 21:20-2Потому что блин оно само undefined, когда переменная создана, но ничего не вписано. С какого причём языка сюда не приди.
(попробую пофантазировать) У вас где-то есть отдельные id, name и desc. Вы обустраиваете их в объект через {id, name, desc}. Если desc задана, но не инициализирована, она попадёт в объект как undefined.
GothicJS
07.12.2022 22:17+1Если desc задана, но не инициализирована, она попадёт в объект как undefined.
А кто виноват в том, что она задана, но не инициализирована?
Devoter Автор
07.12.2022 22:46+1Вы говорите о причинах, в тексте написано - как такой ситуации избежать (используйте
null
), но рассматривается вариант, который реально возможен и встречается в проектах. Оценки плохо/хорошо тут нет. Это может быть сторонняя библиотека, автор которой посчитал, что использованиеundefined
вместоnull
- отличная идея. Ситуация может возникнуть в результате разных обстоятельств: функция, которая должна вернуть объект - вернулаundefined,
а мы записываем это значение в поле нашего объекта, и да, в таком случае, правильно было бы обработать этот результат, скажем так:const prepared = { internal: getAnotherObject() ?? null };
Собственно, цель статьи была обратить внимание на имеющиеся подводные камни. А ваша претензия звучит как "хорошо делай - хорошо будет". Да кто ж с этим спорит-то?
aamonster
08.12.2022 01:31+1Не-не-не, давно уже есть языки, которые за использование неинициализированных переменных дадут вам по рукам...
Так что "с какого языка ни приди" – неточно.
dynamicult
07.12.2022 21:41+6Прекрасно, теперь сериализуем его в JSON, передадим по сети, десериализуем и вновь выполним обход
А ещё в JSON нет типа
callable object,
это значит, что функции вредны и использовать их нельзя? Или они причиняют вам боль? Что насчет BigInt, Symbol и пользовательских типов?
Вы сами нарушили контракт в момент когда решили объявитьdesc?
какstring | undefined.
Потому что первое является соглашением о наличии или отсутсвии поля в объекте, а второе определяет тип его значения. И это не одно и то же. Объекты - не структуры. У них есть цепочка наследования и поля могут перекрываться.
А для контроля сериализации в JSON конкретного типа объекта есть перегрузкаtoJSON
.Devoter Автор
07.12.2022 22:57Мне вообще ничего из этого боль не причиняет, но не раз встречал подобные проблемы у других на каком-либо этапе. Давайте будем честны, с дуру можно сделать много чего, но вероятность того, что разработчик по незнанию попытается сериализовать какой-нибудь Symbol сильно меньше, чем это может произойти с undefined-значением.
С точки зрения интерпретатора что
desc?: string;
, чтоdesc: string | undefined
равносильны, если подскажете способ различить эти определения, кроме как визуально - буду рад. Что касается различных схем, сериализаторов и так далее - ясно дело, что решения есть, правда, чем именно перегрузка toJSON может вам помочь мне неведомо, если вы имеете ввиду, что можно сериализовать undefined-поле как null, то это как раз далеко не то, что нужно, если конктракт подразумевает наличие опционального, а не nullable-поля.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 === 1Devoter Автор
07.12.2022 23:23И что доказывает ваш пример? Что если объявить два разных объекта по-разному, что будет разный результат? Что мешает написать
const example2 = { a: 1, b: undefined } as { a: number; b?: string }
? К тому же, вы вновь аппелируете к TypeScript, когда речь в статье о JS.Upd: извиняюсь, думал, что отвечаю предыдущему комментатору.
Ilusha
07.12.2022 23:53Вы задали вопрос:
если подскажете способ различить эти определения, кроме как визуально - буду рад.
Я на него ответил. Как с позиции TS, так и с позиции JS.
Devoter Автор
07.12.2022 23:58С точки зрения TS - только в одну сторону, а с точки зрения JS - я этот вариант (с проверкой наличия ключа) как единственный способ проверить и привел в тексте публикации, но, хорошо, аргумент принят. Статью поправлю.
Upd: ради интереса проверил, если использовать as, то TS не ругается:
const example2 = { a: 1 } as { a: number; b: string | undefined };
Ilusha
08.12.2022 00:12type 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 значениями.
Второй способ: итерирование по ключам. И здесь это тоже важно.Devoter Автор
08.12.2022 00:24Мой комментарий ниже, вроде как, дополняет предыдущий. Через объявление типа через двоеточие да, будет ругаться. Если же привести к типу через as - не будет, так что даже тут TS не спасет в 100% случаев, не говоря уже о том, что только что десериализованный объект имеет тип any, который приводится к чему угодно, так что дополнительных проверок не избежать. Такое чувство, что вы пытаетесь найти огрехи в коде, который лишь является упрощенной иллюстрацией возможного развития событий. Думаю, если бы я описал еще более подробно, вы бы сказали, что воды столько, что читать вообще невозможно. Что касается stringify+parse, то undefined-поля будут просто удалены, про Vue 2 я имел ввиду всякие Vuex или computed. computed просто не заметит появление отсутствовашего свойства в объекте, даже если он отслеживаемый.
Ilusha
08.12.2022 01:01такое чувство, что вы пытаетесь найти огрехи в коде
Да. Потому что Ваша статья будет проиндексирована и люди будут натыкаться на неё. Мы же не на кухне и не в ламповом чатике)
Я может где-то груб и душнила, за это простите, но без негатива. Для меня хабр с комментами - это в т.ч. в некотором роде образовательная среда.
Type Assertions в ts, имхо, недостаточно строг. Но, возможно, это баланс между функциональностью и строгостью.
Но валидность выраженияconst t = {} as string
лично у меня вызывает вопросы.
Дело не в подробности, а в лаконичной точности с разумным раскрытием смысла. "Вода" - это обозначение того, что иллюстрация проблемы и отход в стороны размазывает суть. Это как насытить предложение множеством разных оборотов....
Никогда на слух не мог воспринять это предложение. Да и при чтении тоже приходится напрягаться.
В белом плаще с кровавым подбоем, шаркающей кавалерийской походкой, ранним утром четырнадцатого числа весеннего месяца нисана в крытую колоннаду между двумя крыльями дворца Ирода Великого вышел прокуратор Иудеи Понтий Пилат.
Devoter Автор
08.12.2022 01:14Ну, к слову сказать, вашими и не только стараниями, кое-какие правки и пояснения внес, за что спасибо, конечно. В данном случае вода необходимо, чтобы было понятно пошагово - что же происходит. Просто на примерах не все могут уловить смысл. Вы читаете с позиции того, кто уже знает, как оно работает, и для вас это - вода. А для тех, кто не знает - должно дать понимание ситуации: возможные причины ее возникновения, последствия и варианты обхода/решения.
Devoter Автор
07.12.2022 23:54Если хотели сказать, что при описании типа как
{a: number; b: string | undefined }
нельзя задать переменной значение{ a: 1 }
, то стоило так и написать. И все равно, мы упираемся в TypeScript, о котором речи не идет.
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
, ссылась при этом на какую-то абстрактную дурость. Таких кейсов с дуракми знаете сколько можно насочинять?
Иметь возможность, что-либо сделать лучше, чем такой возможности не иметь. В первом случае вы можете просто не использовать эту возможность, когда вам этого не нужно. А во-втором случае вы лишаете себя выбора.
Подводя итог, конкретно ваш контракт, каким бы он не был странным, решается простым, хотя бы вот таким кодом.Devoter Автор
08.12.2022 00:42По поводу контракта с обязательным полем тут уже в комментариях поговорили, и текст я поправил, действительно, правильней описать именно как опциональное поле. Сам контракт был приведен просто чтобы описать ожидаемую структуру объекта. Причем тут ваши высказывания о том, что объекты это не просто структуры (C++ вот начал очень сильно возмущаться) мне неведомо. Понятное дело, что есть не только JSON, но любые другие сериализаторы с поддержкой опциональных полей могут вести себя ровно так же, тут, как вы верно заметили, нужна валидация. И вариантов тут два: используется готовая библиотека для валидации (хотя кто-то же ее ранее написал), валидация пишется вручную, или если бы было написано "валидируйте входящие данные", то вопроса бы у вас не возникло? Вот в случае написания проверок вручную и стоит знать о такой особенности поведения. И да, это именно нюанс языка, из которого такое поведение становится возможным. Какие возможности дает наличие undefined, мне, правда, неясно.
Upd: Ах, погодите, в тексте же есть указание на то, что потребуются дополнительные проверки. Или это не валидация?
Devoter Автор
08.12.2022 01:15+1Внес в текст некоторые пояснения, в том числе - упоминание валидации. Надеюсь, это улучшит текст.
OpenA
07.12.2022 21:43+1не знал что парсер JSON выкидывает поля с
undefined
, класная фича же, оказывается от него даже польза есть.что касается примеров то я честно говоря не очень понял в чем конкретная претензия к
undefined
. Он и есть как бы "неопределенный тип" js-рантайм понятия не иммеет какой тип будет уlet a;
, аnull
- это тип object
kichrot
07.12.2022 21:47... Тип
Undefined
явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную ...Лично я считаю, что
Undefined, в некоторых случаях, крайне полезная штука при отладке кода. :)
hello_my_name_is_dany
07.12.2022 22:41+4Ну, потеряли при сериализации мы поле и что? Вы же сами написали в интерфейсе, что поле имеет тип
string | undefined
. Значит, прежде чем его использовать в качестве строки, надо проверить наundefined
Ilusha
07.12.2022 23:04field: string | undefined подразумевает существование поля. field?: string подразумевает, что поле потенциально отсутствует. Это разные контракты.
Практическое значение это имеет, например, в том случае, когда мы хотим итерироваться по ключам объекта.
hello_my_name_is_dany
07.12.2022 23:29В рамках JS это одно и тоже, хотя семантически это разные вещи и соглашусь с вами. Но это не отменяет проверок на undefined.
И если уж честно говорить, то итерация по ключам это слишком грязный хак для решения какой-либо проблемы. Если нет поля, значит и нет значения, тем более раз по интерфейсу так предполагается. Может на фронте часто такие задачи, но на беке редко приходится "рефлексировать".
Рефлексия очень заманчива, но не стоит ею увлекаться. И если использовать, то только тогда, когда есть уверенность на 100% в результатах, что ничего вдруг не сломается. Потому что рефлексия уже предполагает зависимость от данных в рантайме, а в жизни бывает всякое.
Devoter Автор
07.12.2022 23:39И снова, здравствуйте. )) Мне кажется, подобную мысль в комментариях придется еще несколько раз повторить. Цель текста: обозначить имеющиеся подводные камни, на которые стоит обратить внимание, каким способом проблему решать - каждый выбирает сам для себя. Перебор ключей не используется для решения, я с его помощью лишь продемонстрировал тот факт, что наличие или отсутствие undefined-поля можно определить лишь по наличию ключа, но не по значению.Что касается рефлексии, то я тоже сторонник жесткого описания и проверки схемы, причем, как на стороне фронта, так и на стороне бекенда (от нарушения контрактов никто не застрахован). Возможно, в текст стоило включить упоминание того, что в старых фреймворках (без использования Proxy для реактивных свойств) невозможно отследить изменение свойства, если оно отсутствует в объекте (тот же Vue 2.x).
Ilusha
07.12.2022 23:51Я Вам привел в пример, когда это абсолютно разные вещи. Тот же hasOwnProperty будет возвращать разное значение. И есть еще эффекты, мне просто лень набирать доказательную базу. В частности, не просто так существует оператор "delete"
Когда итерация по ключам стала "хаком" или "проблемой"? И почему?
Кмк, отличная история, особенно, когда у нас есть декларативные описания, а порядок не важен для структуры. Или когда есть динамические структуры. Если я не прав, дайте референсы, проведу себе ликбез)
Да, есть зависимость от данных в райнтайме. Только мы всегда от них зависим.
Упрощенно, банальный конструктор форм. Нет поля: нет формы для него. Есть поле: показываем форму.
miscusi
07.12.2022 22:58включаешь строгий режим в TS, перестаешь сравнивать с приведением типа (кроме единственного случая когда null == undefined)
и проблема пропадает почти полностью
Devoter Автор
07.12.2022 22:59Во-первых, про TS сказано почти в самом начале, а во-вторых, в том-то и дело, что "почти". И цель не показать - какие классные решения есть, а указать на нюанс языка, который нужно знать, и который имеет место быть.
somedevfox
07.12.2022 22:58Могу согласится, что
undefined
- не нужная часть языка, но бросать исключение не имеет смысла, потому-что ECMAScript - динамический язык.
В динамическом языке, вы не знаете - с чем работаете, что не правда в статических, какC
иRust
. Даже еслиnull
иundefined
- одно и тоже (исключая сравнения===
), это не отменяет концепт неинициализированных переменных.
Не имеет смысла бросать исключения на тип, который вы не можете знать.Devoter Автор
07.12.2022 23:06Python с вами не согласен
d: int print(d)
От того, что тип переменной динамический, не значит, что такая переменная не существует, это не одно и то же. Не знаю, о каком исключении на тип идет речь, я лишь говорю о том, что при попытке почитать неинициализированную переменную или несуществующее поле, гораздо логичней выбрасывать исключение. В случае с переменной это позволило бы автоматически снизить большое количество обидных ошибок, а для полей объектов есть способы проверки наличия поля.
Devoter Автор
07.12.2022 23:30-1Дополню себя же: если пример с аннотацией не очень удачен, то можно попробовать прочитать свойство, которого там нет из словаря:
d = { 'id': 1 } print(d['id']) // 1 print(d.get('desc', None)]) // None print(d['desc']) // KeyError
Ilusha
07.12.2022 23:10+4Это не JSON.stringify фильтрует undefined, это стандарт JSON не включает в себя такой тип данных. Подозреваю, потому что на другой стороне его может не быть.
Devoter Автор
07.12.2022 23:16-2Вы сами придумали этот вывод? В тексте так и написано, что типа
undefined
нет в JSON. Зачем писать комментарий, если невнимательно прочитали?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 это как значение поля, так и отсутствие этого поля, когда мы говорим про объект.
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));
Не делайте выводов о моем понимании или не понимании, лучше спросите явно. Мой вывод основан на знании других языков программирования, которые прекрасно обходятся без этого типа.Ilusha
08.12.2022 00:03Вы меня простите, но я все-таки сам решу, что читать, а что - нет.
Если бы Ваш вывод не имел продолжения, которое после первой запятой, то я бы согласился. Null был бы предпочитительнее, кмк.
Но то, как "должно работать", не укладывается в парадигму js с позиции типизации.Devoter Автор
08.12.2022 00:09-1Почему не укладывается? Можно подробней?
Ilusha
08.12.2022 00:21Потому что слабая типизация. Рафинированный пример
const t
Да, можно было бы придумать функцию проверки, аналогичную isNaN.
export const test = (param: number) => {
if (!t) { // это обращение к переменной, где Вы хотели бы выкинуть исключение
t = param
}
return t * param
}Devoter Автор
08.12.2022 00:32-1Поздравляю, вы только что запретили использовать
0
в качестве константыt
. Слабая типизация к вашему утверждению не имеет никакого отношения, она имеет отношение к неявному приведению типов, как пример - обычное==
, когда1 == '1'
возвращаетtrue
. Обращение к неинициализированной переменной либо является ошибкой (бросается исключение), либо является неопределенным поведением (undefined - как раз вариант оного). Чем меньше UB, тем проще писать надежные программы.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.
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 базируется на том, пользы от него практически никакой, лишь кое-где позволяет сократить количество кода, зато проблем неопределенное поведение доставляет куда как больше.
GothicJS
08.12.2022 00:07Мой вывод основан на знании других языков программирования, которые прекрасно обходятся без этого типа.
Ну так и без прототипного наследования обходятся, и что теперь, отменить?) JS вообще в некотором смысле язык особенный, это нужно понять и принять)
Devoter Автор
08.12.2022 00:14Все верно, поэтому так и написал: "Нам остается лишь помнить об этих особенностях при написании кода". Утверждение о том, что было бы лучше иначе, не противоречит тому, что есть так, как есть, и надо уметь с этим жить.
funca
08.12.2022 00:02+2Тип Undefined явно лишний, гораздо логичней было бы выбрасывать исключение при попытке прочитать несуществующее поле или неинициализированную переменную, но Брендан Айк пошел по другому пути, впрочем, винить его за это сложно
В первых версиях JS не было поддержки исключений и undefined в результате по замыслу был равносилен ошибке. Если в ваших вычислениях или данных появился undefined, то все дальнейшие манипуляции это UB. Поэтому иметь в результатах undefined считалось плохой практикой (хотя на практике с этим можно жить, до поры до времени, - ваши примеры с JSON и lodash в этом смысле показательны).
В дальнейшем это было несколько раз переосмыслено. Был период, когда void(0) в коде вызывал неодумение. Потом стал паттерном в порядке вещей. Потом придумали разные optional (arguments, chaining и т.п.), сделав вычисления с undefined вполне легитимными. В результате имеем то, что имеем.
space2pacman
Статья для кто работает с JavaScript без документации?
Devoter Автор
Вы прочитали всю актуальную спецификацию ECMAScript? Но, в целом, да, и ремарка насчет игнорирования рекомендаций комитета на это намекает. Реальность не так идеальна, как нам хотелось бы.
MaryRabinovich
Я бы хотела вас поддержать. Документация документацией, но она толстая. Мало кто всё от корки до корки прочёл.
Я обожаю копаться в доке (и не только в ней), но никогда не сталкивалась с десериализацией данных с исходными undefined. Вернее, со сравнением объектов после сериализации-десериализации при таких данных.
Словила инсайт. Спасибо:)
GothicJS
А вы для себя подумайте - почему вы с таким не сталкивались.
Ilusha
А как же сериализация, когда вы отправляете данные на бэк?)
null - поле отправится, undefined - будет отфильтровано.