Comments 67
Тайпскрипт не гарантирует, что API вернёт необходимую строку)
Typescript не кинет ошибку, если бекенд пришлёт невалидные данные.
Вы абсолютно правы! Именно это я и делаю. Опишу контекст последовательно:
Backend может возвращать неверные данные
Значит нужно валидировать эти даные
Для валидации нужно использовать библиотеку для валидации данных
В библиотеке валидации данных решается задача валидации набора строк.
Эту задачу я тут и описал.
А если эта функция чисто для внутренего использования то Тайпскрипт отловит несоответсвия. Но опять же только если ктото просто насильно не указал что входящая переменная типа строка приэтом в реальности не скастовав это — ну или вобще не отключил стрикт моде в Тайпскрипте.
new Set(VALID_STRINGS)
?Джаваскриптеры: мы не пишем на типизированных языках, потому что типы это сложно для понимания.
Тоже джаваскриптеры: держат типы всех объектов и параметров в голове, борются с неявными приведениями типов, пишут гору комментариев о типах, тонну тестов, чтобы проверить, что кривые типы не прилетят в функцию
Да, так гораздо проще и надежнее.
Мне нравится как вы сразу атакуете даже не зная контекста задачи. Проблема возникла внутри библиотеки для валидации данных с API. Где typescript просто не властен ничего сделать)
Как минимум, TS просто не даст вам запихнуть в функцию, принимающую строку, что-то кроме строки.
И опять же, в JS есть уже готовый Set, хоть и убогий.
Да и странный у вас API какой-то, раз вам оттуда прилетают уже готовые объекты JS, еще и разных типов.
const { data: text } = await axios.get('https://some.api.com/valid-string')
if (!test2(text)) {
throw new Error('Invalid text')
}
Typescript не может гарантировать, что бекенд пришлёт необходимый тип.
А, ну земля TypeError'ом
Абсолютно согласен. Но когда нет контроля за API третьих лиц — только так и можно.
const { data: text } = assertValid(await axios.get('https://some.api.com/valid-string'));
И в assertValid проверять нужные типы.
Я конечно не знаю что у вас там за проект, но мне кажется это очень редко где действительно нужно. Как правило с бекенда все приходит правильно и клиентская часть работает.
В случае если придет что-то не то, у вас просто все отвалится. Причем есть у вас валидация или нет — все равно все отвалится.
Получается, что includes() решает вашу проблему и не нужно больше ничего.
Так как я разрабатываю библиотеку валидации, я стараюсь сделать её как можно более широкой в использовании. И как можно более производительной.
Если Set поддерживается браузером — то это хорошо, и его вполне можно и нужно использовать. Скорость проверки O(1)
Но если не так, то Set заменяется полифилом. Производительность которого может быть гораздо ниже. (Я видел полифил, где это происходит за O(n), что мне точно не подходит)
Другой вопрос — что для валидации строк — мне не обязательно нужен Set — и я вполне могу обойтись решением №3. Которое гарантированно быстро и корректно работает.
К слову сказать по замерам на jsperf Решение №3 решение с объектом быстрее. https://jsperf.com/set-includes-object/1
А почему бы не указать в качестве типа принимаегого значения строку? В этом случае при компиляции код, в который прилетает список вместо строки просто не скомпилируется.
Ах, да, в javascript нельзя указать тип принимаемого значения. Ну, мышки плакали, кололись, но продолжали кодить на JS.
Я ответил на ваш вопрос, в разделе FAQ
Я не понял вашего ответа. Возьмите язык программирования, который имеет строгую типизацию. Используйте библиотеку сериализации, которая поддерживает строгую сериализацию. Чем вам, например, не нравится serde? Более строгой десериализации я вообще не видел.
- Взять другой язык — не решит проблему некорректности программы API с третьей стороны.
- Этот код и есть частью библиотеки валидации.
Валидация сторонних данных не требует написания дополнительного кода. Библиотека сериализации использует описание типов данных языка программирования для формирования схемы ожидаемых данных, если они оказываются не ожидаемыми, возвращается Err(), которую нельзя не обработать (не сойдутся типы).
Это код из подобной библиотеки, задача как раз внутри такой библиотеки и возникает, в других проектах — это не нужно
Внутри такой библиотеки… Ну, давайте посмотрим, как они справляются с такой напастью...
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Error>
where
V: Visitor<'de>,
{
self.deserialize_string(visitor)
}
и вот
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self.content {
Content::String(v) => visitor.visit_string(v),
Content::Str(v) => visitor.visit_borrowed_str(v),
Content::ByteBuf(v) => visitor.visit_byte_buf(v),
Content::Bytes(v) => visitor.visit_borrowed_bytes(v),
_ => Err(self.invalid_type(&visitor)),
}
}
Неужели строгая типизация это так сложно? Ведь компилятор за вас думает. Остаётся только подбирать такую комбинацию кнопок, чтобы компилятор не ругался, и код правильный. Без ошибок типизации.
Это не сложно) когда это строгая типизация была сложной?
Другой вопрос что на фронте редко когда проект написан на подобном языке
(обычно сложности начинаются с higher order kindness, и типопараметрами для трейтов).
Вот у меня и есть экзестенциальный вопрос: если на языках со строгой типизацией легче писать код без WTF, то почему все пишут на языках с WTF?
Я писал интерпретатор на Haskell, так что для меня эти слова не новы, по крайней мере не страшны. Кайнды и классы типов(возможно нечто схожее с типопараметрами, но это не точно) идеи знакомые. Rust не пробовал.
Во первых JavaScript плохо продуманный язык изначально. Но он не лишён плюсов.
Он однозначно полезен для своих задач. Если бы у него не было бы плюсов, его бы давно заменили.
Тем более, когда он пишется с помощью тайпскрипта.
Конечно это не настолько строгие типы, и wtf возможен.
Другой вопрос что для бизнеса выгодно выбирать наиболее распространенную технологию.
Итого статьи:
- javascript — язык с динамической типизацией
- это — подводный камень ©, потому что
- может происходит неявное преобразование типов
- поэтому типы данных желательно проверять
Всё верно?
PS. от меня всё еще ускользает смысл статьи — что такого неочевидного ("подводный камень"), то есть не описанного в документации по JS, было обнаружено?
Использование Javascript — одна из предпосылок статьи, так как он является дефакто стандартом веб разработки.
Но дело не в его динамичности, а в способах гарантии валидности данных приходящих по сети. Какой бы язык я не выбрал для своей разработки, он не даст мне гарантий корректности чужой программы(Бекенда).
Решение этой задачи необходимо внутри кода библиотеки валидации.
— Вы уверены что поиск ключа объекта (ну и всей цепочки его прототипов) более быстро срабатывает как поиск на полное совпадение в статическом массиве? Тем более после разогрева.
— Array.includes всетаки более предпочтительно так как Вы явно подсказываете что там массив, и что Вы с ним хотите делать. Таким образом давая шанс движку сделать свои микрооптимизации. А также легче читается. А если вдруг ктото незнает что какая сложность у Array.includes то можно коммент добавить :)
Вы правы, в каждом конкретном случае это решение может быть медленее(если массив достаточно мал)
Я не краду у оптимизатора возможность оптимизировать. Я меняю ту операцию, которую он должен оптимизировать с "проверка на наличие элемента в неизменном массиве" на "взятие по ключу в неизменном объекте"
Думаю, что includes может быть оптимизирован до O(log N) или даже O(1), если он учтёт, что массив не меняется. Но предполагаю, что моё решение с изначальным O(log N) будет оптимизировано ещё больше.
Object.create(null)
чтоб удалить все дефолтные свойства объекта. А то там ещё немного камней оставили.
test = {};
!!test["constructor"] //=> true
Задача
Дан список строк: VALID_STRINGS.
Cоздать функцию валидации test(x) которая должна вернуть true, если x — это одна из строк в этом массиве.
Область применения: x — любое значение Javascript
Ограничения: Не использовать ES6. (Цель — старый браузер)
Самым простым решением, которое может быть — это пройтись по всем строкам в этом массиве и сравнить.
Это решение правильное, но медленное, потому что оно заставляет при каждом вызове функции пробегать по массиву в поисках совпадения
Простите, а чтобы положить все строки из списока в словарь, Вам не нужно по нему пробегать? В вашей задаче явно указано, что на вход приходит список, а не словарь. Также нигде в условии задачи не указано, что вам нужно оптимизировать многократную проверку на одном и том же массиве.
Как по мне — ваше решение указанной в статье задачи является переусложненным
Статья скорее про баг, чем про решение задачи.
Я встретил баг, который я сразу не предвидел, что называется "подводный камень"
И решил о нем рассказать, дело даже не в задаче, оригинальная задача требует именно наибольшего перформанса.
Моя ошибка была в том, что я предположил, что если я создаю объект с ключами строками равными true, то только эти строки будучи используемы как ключи дадут ожидаемый результат.
Что оказалось не правдой.
Поэтому я статью и написал
Создайте объект с 50 атрибутами
const obj = {a: true, b: true, c: true, ...};
Далее создайте массив с ключами
const arr = ['a', 'b', 'c', ...];
Теперь запустите в консоли
console.time('obj'); console.log(obj['c'] === true); console.timeEnd('obj');
а потом и для массива
console.time('arr'); console.log(arr.includes('c')); console.timeEnd('arr');
И вуаля! Скорость почти одинакова!
На вопрос почему так. Полагаю что дело в HashMap таблицах под капотом в движке. Но это не точно :)
PS заранее извиняюсь, что нет решения для старых браузеров. Просто хотел обратить внимание, что объект как ключ возможно не улучшит перформанс.
Интересный пример, а попробуйте замерить тот ключ который отсутствует
Я когда-то тоже так делал, как автор статьи. Заводил индексный объект, как я его тогда называл. Думал, что перфоманс будет улучшен. И вот в один день я проводил лекцию для джунов, где делился всякими фишками по JS. Ну и типа что бы показать, что можно улучшить, сделал такой тестовый пример. И когда замерил — ой! Реально был удивлен. Так что профита такой подход особо не приносит, и я от него отказался в последствии.
Хотя, надо на досуге покопать. Может можно как-то заставить это работать быстрее.

Такие результаты вполне однозначно говорят о том, что проверки объекта соптимизировало до констант, только и всего.
Не пытайтесь делать далеко идущие выводы о скорости js, запуская какие-то синтетические тесты, не похожие по данным и принципам выполнения на вашу реальность.
Одно дело, когда оценка временной сложности сомнительна, и есть какие то поводы полагать обратного. Но не так в нашем случае.
С одной стороны, замеры скорее подтверждают, что массив это О(n), а объект О(1) или О(logN).
С другой стороны, существенных доказательств обратного я не увидел.
Но предположим, что массив будет оптимизирован до константы или логарифма, это всё равно не сделает его более подходящим для этой операции, ведь объект гораздо более будет ускорен, ведь у него изначально сложность константа (или логарифм).
С одной стороны, замеры скорее подтверждают, что массив это О(n), а объект О(1) или О(logN)
Почему вы упорно продолжаете игнорировать Set, который как раз и должен быть O(1)?
С другой стороны, существенных доказательств обратного я не увидел.
Если для вас не является существенным, что согласно этим тестам объект почему-то на порядок производительнее сета — ну, удачи продолжать в том же духе. В конце концов такие заявления не фальсифицируемы, говорить «не вижу существенных доказательств, почему Юпитер в третьем доме не может ускорить мой код» — не возбраняется.
Очень просто:
Если Set в браузере имплементирован, Set выдаст результат асимптотически не быстрее чем решение с объектом.
Если Set в браузере нет, то Set(полифил) выдаст результат в наилучшем случае, со скоростью объекта.
Те замеры, на мой взгляд дают мне право выбрать решение с объектом, которое не хуже справляется с моей задачей в современных браузерах, и справляется лучше на старых.
(А так как я пишу библиотеку для меня важно, чтобы поддержка библиотеки и её производительности была на многих браузерах)
Если он в браузере имплементирован, он не выдаст результат асимптотически быстрее чем решение с объектом.
Господи боже мой. Вам показывают картинку, на которой некий показывающий погоду на Марсе «бенчмарк» выдаёт якобы десятикратный перевес объекта над сетом, а вы в ответ пишете «он (сет) не выдаст результат асимптотически быстрее, чем решение с объектом». Автор, вы вообще читаете, что вы пишете?
Вам пишут, что «тесты» на которые вы ссылаетесь — фуфло, потому что выдают фуфловые результаты, вы в ответ излагаете какое-то фентези на тему «почему я не взял сет», которое к обсуждаемому вопросу отношения не имеет.
Не нужно вообще ссылаться на какие-то замеры производительности, если с первого взгляда понятно, что эти замеры замеряют непонятно что.
Вы же понимаете, что просто сказать, что тесты — фуфло, не предоставив доказательств — не аргумент. И вы ещё осуждаете меня за то, что я не верю вам на слово, на мой взгляд преждевременно и несколько напыщенно.
Те доказательства, которые вы привели — замер на малых числах при не наихудших условиях, который показывает сравнимую производительность в таких условиях(с чем я полностью согласен), но отнюдь не ассимптотику времени выполнения на этих структурах данных.
Бросаю вызов: скиньте хоть какое то исследование или тест, которые покажут, что неизменный объект с константным количеством прототипов в цепочке выдает не константное время взятия по ключу. И что в этих же условиях Set выдаст константную ассимптотику.
Вы утверждаете, что я глуп в своих суждениях, но не представляете аргументов, помимо вашего собственного мнения — не надо так)
Мой выбор опирается не на бенчмарки, хотя они его и поддерживают.
Мой выбор опирается на то, что внутри объекта, равно как и внутри Set'a используется хештаблица. Которая даёт константный доступ к ключу.
А внутри массива — структура данных массив. С линейным алгоритмом проверки на наличие(не сомневаюсь, что оптимизатор может оптимизировать и до логарифма и до константы).
Внимание, подводный камень