Giter Site home page Giter Site logo

blog's People

Contributors

corysimmons avatar

Watchers

 avatar  avatar

blog's Issues

Why Typescript?

image

Is it not a strange fate that we should suffer so much fear and doubt for so small a thing?

Typescript has been around since 2012, but it wasn't until a few years ago that it really started picking up speed. A lot of famous JavaScript library developers started switching to it (particularly in the Angular, and then React, communities—and now everywhere). This was fine to ignore at the time because it didn't affect me. But in the past few months I keep noticing this trend in job descriptions: "extensive Typescript experience".

I'm not new to types. I wrote a pretty large open-source project using Flow (Facebook's challenger to Typescript), and I remember thinking Typescript was better for a few reasons:

  1. In Flow you can cheat and just ignore typing half of your project (which doesn't make it very reliable). You can do this in Typescript too, but it requires the malicious effort of going through and adding the any type to everything, whereas you can just "forget" to type certain files in a project with Flow. So Flow's biggest selling point is actually a weakness.
  2. Typescript has a massive collection of types for various popular npm packages out there. For instance, React. Go to https://microsoft.github.io/TypeSearch/ and search for some of your favorite libraries.

But what is Typescript?

In programming, there are two types of languages: loosely typed and strongly typed.

JavaScript is loosely typed. If I say var x = 1 then I (or some library) can accidentally re-assign that x var doing something like var x = "1" later on.

So think about that for a second. You are expecting x to be a number of 1 but it got changed to a string. This can frequently introduce bugs. Like... you are lying, or new, if you haven't ran into a bug like this hundreds of times in the past.

Just for type safety, I wouldn't say introducing Typescript is worth the overhead. I don't run into type bugs very often so it's not a huge selling point for me. The real benefit of using Typescript is all the annotations your code editor gives you now. Holy crap your code editor will light up like a Christmas Tree.

So I create an type interface (which is just something that looks like a JS object) and then feed that into a React component, and 💥—if my component doesn't have the right props passed into it, then VSCode let's me know.

Untitled

Now I never have to worry about if I forgot a prop that my component needs.

But wait, there's more!

These type definitions also act as pretty damn good layer of automated documentation.

Untitled 1

I'm having a hard time holding option and hovering that IPost while simultaneously doing the keyboard shortcut and dragging to get a screenshot, BUT IF I COULD, it would show you a popup with:

export interface IPost {
  title: string
  body: string
  author: string
}

Think about how handy that is. 90% of the documentations we use are API References, and this is effectively that.

Think about that co-worker who writes all kinds of spaghetti and never documents it. Now you'll know the required (and optional) parameters, and types, of every function they push to the project. 🔥

Does this mean docs are dead?

There's still a place for a greeting page with a getting started and some demos, but for the most part, yes, people should stop writing docs. Traditional docs are frequently outdated/broken and frequently a huge source of frustration for me.

Typescript can't lie or forget to update docs, so, combined with decently-named functions and parameters, it's strictly better.

It looks hard.

It is and isn't. It's intimidating at first, so I highly suggest you make a ☕️ then speed-watch a video course on it to at least have a compass.

Then it takes a small project to get used to, but after that you'll be competent enough to fall in love with it like so many have before you.

You really don't have a choice. Like I said, Typescript is more-and-more frequently appearing in job descriptions, and being used in every significant project on GitHub. Unless you're the CEO/CTO, it's only a matter of time before you're asked to learn it. Might as well start now. You'll like it. 😇

Stay safe. ❤️

The Testing Ecosystem is Frustrating on its Best Day

image

I'm either dumb (likely) or our current practices with testing and the entire ecosystem is pretty broken. I've always wanted apps I work on to be really well tested, but it feels way harder than it should be.

Why Test?

First, why should we write tests? What's the point? What do we get out of it?

There is a two-part answer that addresses all of those questions:

  1. We can sleep better at night.
    • Knowing our users don't have to fight with our app.
    • Knowing our laziness isn't causing real frustration, stress, and pain.
    • We're not introducing more cortisol to the world.
    • We're not making the world a worse place (when people are stressed they take it out on everything around them).
  2. We don't cause regressions.
    • When we (or someone on the team) changes something, it doesn't cause a different page in the app to break or display the wrong data.
    • We don't have to manually QA every single possible state of our app over-and-over-and-over again.

That's it. We want to do our best to make sure our users aren't fighting with our app, and we don't want to have to check every single little thing every time someone opens a Pull Request.

With those two goals in mind, let's proceed with some of my gripes before getting to a solution, then get back to more griping.

Stop Mocking

Let's get this over with because it's one of my biggest pet peeves in the testing community.

Mocks are one of the stupidest concepts I've ever tried to wrap my head around—and I have tried for almost a decade, asking many professionals, because I thought I was simply missing something, but it turns out I was right and mocking really is just dumb for the most part.

The idea behind mocks goes something like...

Let's test something, but instead of using the function or data that the real app would use, let's just completely make something up that way it is faster and easier to set up.

How does this actually confirm whether a user is experiencing your app the way it was intended?

Think about it, you want to test whether a user can create an account on your website, so you begin writing a test for it and realize you have to hit your API to actually create the account in the database.

Well, you don't want to actually create the account in the database, so instead, you just pretend the API did what is was supposed to do, and everything works as expected.

Sweet. Your tests pass and everyone is happy until thousands of users can't make an account. How would you know your API had changed? How would you know the frontend needed to change to reflect the API changes? You never actually talked to your API. Your tests always just played around with hard-coded JSON in a make-believe fantasy world. 👇

image

Are mocks easy to setup and use? Yes. Are you purposefully tricking yourself and every company stakeholder into thinking your app is well-tested? Yes. This isn't a good trade-off.

So what's the alternative? How do you actually test forms and APIs and stuff without having to actually inject junk into the database?

Set up a Proper Test Environment

There's no way around this. If you actually want to test your app in a confident manner without dangerously cluttering your production db with fake data from your tests, you have to set up a test environment.

This takes a bit of setup, but once you nail the pattern, you can create a custom test env for any app. It mostly revolves around replicating your database and creating pretty thorough seed files with plenty of real-ish data.

You want to work with as much real-ish data as possible. You can create a script to:

  1. Delete the old test db.
  2. Get a SQL dump from production.
  3. Replace PII with fake PII.
  4. Import that dump into a fresh db.
  5. Run this as needed (preferably daily through a CRON job until development on the app slows down) and commit it to repo to keep your team on the same page, and your test data fresh. Without fresh, production-ish data, how is our approach different from mocking data?

You can/should use this script for your staging env as well to keep it as up-to-date with real-ish data as possible too.

Warning Please be sure to have backups upon backups of your production db in case you screw something up and need to rollback. This shouldn't need to be said. If you find it difficult to setup a CRON job to get a production db dump and send it to S3, then at least make sure your db host are backing up your db (you will probably have to pay a bit for this service).

Now you can confidently create a test db, do whatever you want to it, then delete it. You can do this billions of times, locally or in CI, with or without considering a team, and do whatever you want to that db.

When is mocking okay?

Almost never. Stop going back to your evil mock ways.

"What if I have to communicate with a 3rd party API?"

This is going to hurt you because it's extra work, but just create a test account for that 3rd party API, add that API token to your .test-env file. And then create another script to export production data from the original account, fake PII, and import it into the test account.

Now that I think about it, we should only be exporting data (from a production db or a 3rd party API) via whitelisting records. PII can always be introduced to an API.

This might seem like overkill, and if you're only using the API for a couple payloads, then mocking is probably fine, but if your app heavily relies on specific 3rd party APIs, then the freedom to test against their actual endpoints, libraries, and SDKs will save you time, and quickly catch a lot of bugs, in the long run.

As an example, imagine your app sells stuff or takes donations and relies heavily on Stripe. You haven't upgraded the Stripe SDK dependency in your app in a while. Let's say you're on something like 2.0.3 (these are imaginary semver versions I'm picking); several months go by and your tests have been passing; someone hacks Stripe; Stripe fixes it quickly and releases the fix as 4.3.4.

How do you confidently upgrade to 4.3.4? You have 400 tests that were mocked with payloads you got from 2.0.3.

Do you go through every single test and compare its request and response to the 4.3.4 version, and tweak the mocks to match 4.3.4? You will almost certainly make a mistake that won't be caught until something happens enough times in production that a user finally speaks up. You've potentially done millions of dollars of damage to your company.

On the other hand, if you had a test env setup from the beginning, everyone on the team could've been upgrading willy-nilly the entire time. You're not messing with real money (Stripe also appreciates the concept of test environments so you just change your Stripe API secret key in your .test-env file et voila) so who cares if something breaks in your chore/upgrade-stripe branch?

Is it ever okay to mock?

Fine. These are the only three instances I can see mocking being useful.

  1. Again, if you are hitting a 3rd party API for a couple small payloads of something really, truly, insignificant to your app, and you are coding defensively on the frontend to simply not show that data if that API broken.
  2. If a 3rd party API is just way too expensive to justify creating an extra testing account for it.
  3. If a 3rd party API doesn't have any concept of test envs; has a really bad API for exporting/importing data; or they don't adhere to semver very well. In all cases it's really hard to write high-quality export/import scripts for your test env. I would advise searching for an alternative service or even just recreating what they do for your own company's specific use case.

Let's stick a pin in the whining for a second to go over some testing approaches and why they fall short.

Testing a Component in React

Why mount a component at all? Why do this? Does knowing a component has x prop confirm the end-user is seeing a specific view the way it was meant to be rendered?

Pseudo example:

// some-page.tsx
const columns = ['Employee', 'Salary']
const cellData = [ /* ... */ ]

<Table
  columns={columns}
  cellData={cellData}
/>

// some-page.test.tsx
test('Column header should be Employee', () => {
  const columns = ['Employee', 'Salary']
  const mounted = mount(<Table columns={columns} />)
  expect(mounted).toBeVisible()
  expect(mounted.props.columns).toEqual(columns)
  expect(mounted.someSelectorFunc('th:first-of-type')).toHaveText('Employee')
  // ✅ Passes!
})

Sweet, it passed. Does this test, or any component test really, 100% confirm a user is able to see that Employee?

  • Does it render it correctly with the correct CSS? Can you look at a picture or load the state of the app at the exact moment the assertion was executed to confirm it looks correct?
  • Is it actually visible-visible? I've had plenty of false positives (and negatives) where simply checking visibility was super frustrating to check for, then flaky.
  • Does confirming the props are XYZ actually mean anything?
  • It's annoying we have to type so much code to simply test "If I look at this page, it should have a header that says Employee." And holy crap once you start actually clicking on things and waiting for elements to hide, pages to change, elements to transition into view, etc. it becomes a huge pain. Like, look at all the stuff you have to do to click on a button and see if it triggered some element to be modified: https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning

So combos like Enzyme or Testing Library + Jest are pretty useless.

Kent C. Dodds seems very smart when it comes to testing, and probably the most famous person in the "testing JS" sphere (in fact he has a course called Testing Javascript), but even his testing library seems like it only provides a simpler way to mount a component then adds a couple shortcut functions that just abstract CSS selectors a smidge (shout-out to trying to make devs more a11y-friendly by promoting selectors like "findByText" and "getByRole").

Don't get me wrong Testing Library is 1000x easier/better than Enzyme, but it still is just looking at some JSDom, and working with that JSDom is incredibly frustrating.

All of this said, Jest is actually pretty amazing. It is super lightweight, fast, has a really nice watch mode, has a nice colorized CLI, all kinds of configuration options, etc.

Jest is just a great tool. Good job Jest.

E2E falls short

At first glance, E2E seems like a good idea:

  1. Open a real browser.
  2. Interact with a website.
  3. Confirm things look a certain way.

Tools like Selenium, Nightwatch, TestCafe, Cypress, Playwright, etc. have been around for a very, very, long time. People have been using them to create all kinds of tests for a decade or so.

A few problems with these...

If you run these headed (i.e. you can see the browser pop up and click around) then they are slllloooowwww. It just takes the browser forever to start.

We still have to write code to click things, interact with forms, etc. but at least we don't have super weird/awkward stuff like act to fight with.

Honestly, Cypress handles state change like I would expect.

Look at this https://www.cypress.io/blog/2019/02/05/modern-frontend-testing-with-cypress#toggle-completed-state

That's exactly what I want. To be able to write a test like this:

test('When the Create Account button is clicked, it gets removed from the page, and a loading spinner gets added', () => {
	const btn = find('#create-account-btn')
	btn.click()
	expect(btn).not.toBeInTheDocument() // even if it animates out
	expect(find('#spinner')).toBeInTheDocument() // even if it animates in
})

Cypress even waits on animations and such.

I cannot emphasize how much I hate that act() warning so Cypress immediately beats everything.

Imagine trying to learn exactly when/how/where to use act() and all the gotchas, and then teaching your team that. It won't happen. It isn't worth it. Just use Cypress.

Downsides to Cypress

Cypress is slow. Run Cypress in headless mode via cypress run (instead of cypress open).

Cypress is also a real pain to get working in Docker (especially on M1 Chips). I half suspect it is on purpose. If their official Docker images worked, why would anyone purchase their Cypress Dashboard product? I wouldn't. So there is a lot of lock-in with Cypress.

Playwright was really close

Playwright looked like the best of all worlds:

  • E2E
  • Fast, lightweight
  • You can Dockerize it pretty easily.
  • Great API that waits on elements to animate and change state and such (like Cypress does).

Playwright doesn't work with Yarn 2's PnP. Its codebase is riddled with references to node_modules (which don't exist in Yarn 2 PnP) and like most OSS maintainers, no one is excited to upgrade their project to support a specific tool.

Playwright can't into Typescript paths: microsoft/playwright#7121

Screenshot testing

I keep coming back to this idea that the answer to all my testing problems revolves around screenshots somehow. My reasoning is what if I forgot to test for a sidebar visibility in an E2E test suite? Would the E2E runner care? No. But with screenshots, yes it would. It would also test for the visibility of certain elements without me having to actually type that stuff out.

For instance, in any other test runner, I'd have to do something like:

test('Clicking the submit button shows a success message and removes the submit button then adds a new newsletter signup form', async () => {
	find('#btn').click()
	expect(find('#success_message')).isVisible()
	expect(await find('#btn')).not.toBeInTheDocument()
	expect(await find('#newsletter_form')).toBeInDocument()
	expect(await find('#newsletter_form_checkbox')).isVisible()
	expect(await find('#newsletter_form_checkbox').attr('checked')).isFalse()
	expect(await find('#newsletter_form_btn')).isVisible()
	expect(await find('#newsletter_form_btn').attr('disabled')).isFalse()
})

What if I could combine everything after the "Do some interaction" part, every assertion (and if you're writing really thorough tests, you need to write a lot), into a single assertion like so:

test('Clicking the submit button shows a success message and removes the submit button then adds a new newsletter signup form', (oldSnapshot) => {
	find('#btn').click()
	wait('500ms')
	const newScreenshot = screenshot()
	expect(oldSnapshot).isSameImage(newScreenshot)
})

👆 So much easier. Any dev can pick this up immediately. Multiple that by hundreds of tests in a project.

There are projects like:

Percy and Happo are expensive and lock-in. Chromatic is expensive and lock-in, and only works with Storybook (sidenote: I'm not a big fan of Storybook or Docz either, you can probably generate cleaner/more-helpful docs pretty easily from types, mocked 😱 data, and some loop fn).

jest-screenshot sounds very promising but it's unmaintained and I ran into another Yarn 2 problem Prior99/jest-screenshot#83

If jest-screenshot is to be believed then jest-image-snapshot is slow. I also had some flaky tests with jest-image-snapshot.

Alas

Almost everything in the 2021 JS ecosystem feels off (with the exception of a few tools like NextJS which is so nice).

I don't think my ideal testing tool exists right now.

  • I want something simple and lightweight so I can plop it in Docker and introduce it to CI environments pretty easily.
  • I want something where the API automatically waits for elements to appear and times out after defaultMs = 1000 like Cypress, Playwright, and https://testing-library.com/docs/dom-testing-library/api-async#waitfor do.
  • I love Jest's colorful, clean, diffing, CLI tool and the various types of watching it can do (--watchAll, incremental, etc).
  • It can't reference node_modules and has to be Yarn 2+ friendly.
  • I want it to take screenshots using all the fastest libs (so it's "10x faster" than jest-image-snapshot) so I can ensure there aren't visual regressions (hugely important) but also just completely sidestep having to write a ton of ugly assertions.

I think it might be pretty straightforward to develop by forking jest-screenshot, or just creating something from scratch using some of the libraries jest-screenshot uses.


Babel


pubDate: 2016-06-23

image

There are new versions of Javascript (ES6, ES7) full of new features and a ton of syntax shortcuts. For example:

() =>
  2

...is shorthand for an anonymous function that returns 2.

As with everything in programming, people can't agree on anything and browsers are slow to implement stuff. We'd have jetpacks before browsers implemented it correctly/consistently so some cool people made some things called "transpilers" that convert ES6+ to usable old-fashioned Javascript (ES5). The most popular one is Babel.

Here's what it converts the above code to.

Babel works with just about any build process, and is extendable with modular plugins.

How to use it

The easiest/cleanest way is installing some npm packages and configuring it in package.json. We'll make an npm script for it so we can run something like npm run dev from the terminal.

  • cd ~/playground/babel
  • echo '{}' > package.json (create a valid JSON file so we can npm install to it)
  • npm i -D babel-cli babel-preset-es2015 babel-preset-stage-0 (this will install Babel's CLI, ES6 (aka: ES2015), and ES7 support)
  • Add a "babel" object to package.json. Inside it we'll tell it to use the presets we installed:
"babel": {
  "presets": [
    "es2015",
    "stage-0"
  ]
}
  • Create a test.js file and put it in a src folder. src (sometimes lib) is your original code. We transpile stuff from src to the sibling dist folder.

Protip: You transpile CSS (Sass, Stylus, LESS, PostCSS, etc.) and HTML (Pug/Jade, Haml, etc.) as well. Currently most companies organize this stuff as src/css, src/js, src/html, etc. but I think component architecture is a better solution for larger projects so try it out if you can.

  • Now run node_modules/.bin/babel src -d dist to ensure ES6/7 files in src are being transpiled to vanilla Javascript in the dist folder.
  • If you run node_modules/.bin/babel -h you'll see it has a watch flag.
  • Run node_modules/.bin/babel -w src -d dist and change some stuff in src/test.js to make sure it's being transpiled on saves.
  • Now... it'll suck to have to type all that crap every time so add it to the "scripts" object in package.json as "dev" then just run npm run dev (package.json scripts don't need the node_modules/.bin/ part).
  • You can add/nest extra files in src and everything will work fine.

package.json should look like this (if you copy this make sure you run npm i before using it):

{
  "devDependencies": {
    "babel-cli": "^6.10.1",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-stage-0": "^6.5.0"
  },
  "babel": {
    "presets": [
      "es2015",
      "stage-0"
    ]
  },
  "scripts": {
    "dev": "babel -w src -d dist"
  }
}

Congrats

Now you're able to use ES6/7. Not only that, but you're using package.json scripts to perform tasks which is pretty nice as long as a CLI for a particular tool is available (if one isn't, consider doing everyone a favor and making one).

Homework

  • Learn ES6/7. It's awesome and will make your code much more elegant.
  • Learn how to use npm-run-all to watch/transpile Babel and Sass at the exact same time and consider completely replacing Gulp/Grunt with npm scripts.
    • Make Sass output source maps to dist/css/maps folder.
    • Make Sass build everything in src/css before it watches.
    • Answer Gist

ESLint


pubDate: 2016-06-22

image

Linting is cool. It keeps people from making stupid mistakes and can make your code a lot neater/easier to read.

Not linting is how you end up with spaghetti code that no one can read.

Lint everything. HTML, CSS, JS, Python, Ruby, PHP, etc.

ESLint lints JS really nicely.

There was JSHint and JSLint, but they were hard to extend so someone made a JS linter that was easy to extend called ESLint. It's now the standard.

Agree with your team on a coding style. This part isn't super important and there are a ton of well thought out configs out there already like AirBnB. What's important is you all agree to do the same code style!

How to use ESLint

Use npm. It's easy and portable.

  • cd ~/playgrounds/eslint
  • echo '{}' > package.json
  • npm install --save-dev eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y eslint (AirBnB usage)

Note: I feel ya. All of these npm packages seem silly, but it's the whole Unix philosophy of making everything modular. It's a bit annoying, but overall it's usually a good thing.

  • At this point you could do something like eslint --init to make an ESLint config file. In that file, you could add something like "extends": "airbnb", but that's newb stuff. Do we really need yet another config file?
  • Do this instead: in your package.json add an "eslintConfig" object with "extends": "airbnb" in it.

Protip: If you're configuring some npm package, see if you can put it in package.json instead of having 20 config files polluting the root of your project.

  • Now you could run node_modules/.bin/eslint in terminal (and this is actually cool/useful for the --fix flag), but you don't want to be switching between terminal and editor after every keystroke to see if you made a mistake, so install an editor plugin for ESLint. Here's Atom's. ESLint is insanely popular so there is almost certainly a plugin for your editor.
  • Restart your editor and type some sloppy, error-filled, JS. Your editor should light up like a Christmas tree.

What if I have some global variables? jQuery doesn't work! alert() is throwing an error!

Config it!

"eslintConfig": {
  "extends": "airbnb",
  "env": {
    "jquery": true
  },
  "rules": {
    "no-alert": "off"
  }
}

Congrats

Now you're linting your JS like a pro. Enjoy your impending OCD.

Homework

  • Experiment with some other configs (standard is currently my favorite).
  • Write your own config.
  • Skim through the rules and read what some of them do.
  • Skim through some configuration options so when something breaks you might have an idea as to what's wrong.

Building a Simple Typechecker


pubDate: 2016-10-12

image

Type-checking is pretty sweet and helps prevent lots of hard-to-find bugs. If you're using default named params for all your functions, it's fairly easy to integrate some simple type-checking.

A real-world example might make this more clear!

Frankenstein (the doctor -- not the monster) has some cats. Some of them have less legs than they are supposed to have. He can add more legs, but he developed some compulsive disorder and can only add legs if he's allowed to double them. As @mpjme would say, this is the kind of problem you encounter every day in Enterprise-level Coding™.

const typeCheck = (interface, argsObj) => {
  for (const param in interface) {
    if (typeof argsObj[param] !== interface[param]) // Comparing the arguments to the type interface.
      throw new TypeError(`${argsObj[param]} should be a ${interface[param]} type!`)
  }
}

// Type interface
const kittenTyping = {
  nickname: 'string',
  legs: 'number'
}

const frankenKitten = ({nickname = 'Hops', legs = 3}) => {
  typeCheck(kittenTyping, {nickname, legs}) // interface, argsObj
  console.log(`*sewing sounds* 🙀  Now ${nickname} has ${legs * 2} legs! 😻`)
}

// Lets saw a leg off so Hops ends up with a normal amount of legs (4), then let's rename him Skip.
frankenKitten({nickname: 'Skip', legs: '2'}) // TypeError: 2 should be a number type!
frankenKitten({legs: 2}) // Ah, that's better!

Now we're catching type bugs that otherwise would've leaked through since clever JavaScript will "intelligently" (aka: magically) convert that '2' string into a number during the multiplication step.

Protip: Place typeCheck() right above return statements to help prevent bugs. If you encounter a bug, just add typeCheck()s higher and higher in your code until you find the culprit (then clean up after yourself except for that one right above your return). Psst, the culprit usually looks like Object.<anonymous> in the terminal stack trace.

This isn't great because it requires us to pass all the interface params to argsObj whether we're using them or not.

// Hops is still a good name!
frankenKitten({legs: 2})

// ...

typeCheck(kittenTyping, {legs}) // Error! We always need to pass whatever params match in the function defaults and the interface.
typeCheck(kittenTyping, {nickname, legs}) // Works, but smells a bit.

I'm sure there's some clever way to capture deconstructed variables without having to explicitly pass all of them within typeCheck(), but tbh, this solution isn't as nice as existing tools and I'm getting hungry.

It is, however, a pretty good way to start sneaking basic type-checking into stuff without introducing any dependencies.

It also educates and promotes the discussion of integrating one of those more feature-rich solutions. Coworkers should begin to see the value in type-checking, but probably feel kind of gross about all the typeCheck() functions littering your code, or they'll want to typecheck arrays and such...

That's the purrfect time to sell something like Flow/TypeScript/Elm. 😽

Vanilla JS Deep Object Comparison


pubDate: 2019-10-17

image

I very randomly got curious how to compare objects in Javascript so opened DevTools and began tinkering. I remembered you could use JSON.stringify() to do this, and it even works with deep equality. Example: JSON.stringify({a: 1, b: {c: 2}}) === JSON.stringify({a: 1, b: {c: 2}})

But it breaks the second those properties aren't in the same order. For instance, JSON.stringify({a: 1, b: 2}) === JSON.stringify({b: 2, a: 1}) fails.

Like most deep things, I figured a recursive function would be necessary, and out of laziness, started Googling around to see if there was a really slick ES6 way to do it (like some fresh/obscure Object.deepEqual native method I hadn't learned yet or something), but alas, there wasn't.

I was certain Lodash had something. And of course it did. And in practice, I would probably use it because it's probably better tested for fringe cases, probably pretty performant, and Lodash is familiar to all developers and comes with lots of usage documentation.

But I was curious how it worked so I dove into Lodash code for a second and it was depending on sooo many little stupid building block modules for every single thing, that reverse engineering it made the problem "not fun".

So, I just figured I'd try to solve it myself (which I'm proud to say I did really quickly—usually these things hamstring me for a couple hours but I solved this one instantly) and submit it to StackOverflow (since its an incredibly popular question, and my solution is elegant, maybe I could get some of that sweet SO karma), but all the threads for it were locked, so HERE WE ARE. 😩

const nestedObj1 = { b: 2, c: { e: 3, d: 4 }, a: 1 }
const nestedObj2 = { a: 1, b: 2, c: { d: 4, e: 3 } }
const nestedObj3 = { a: 1, b: 2, c: { d: 4, e: 9999999999999 } }
const shallowObj1 = { a: 1, b: 2 }
const shallowObj2 = { b: 2, a: 1 }
const shallowObj3 = { a: 1, b: 9999999999999 }

// Recursively sorts the keys and returns a fresh object
function sortObj(obj) {
  return Object
    .keys(obj)
    .sort()
    .reduce((acc, curr) => {
      if (typeof obj[curr] === 'object') {
        acc[curr] = sortObj(obj[curr])
      } else {
        acc[curr] = obj[curr]
      }

      return acc
    }, {})
}

// Does that stringify comparison stuff to the _now 100% recursively sorted_ objects
function deepEqual(obj1, obj2) {
  return JSON.stringify(sortObj(obj1)) === JSON.stringify(sortObj(obj2))
}

deepEqual(nestedObj1, nestedObj2) // true
deepEqual(nestedObj1, nestedObj3) // false
deepEqual(shallowObj1, shallowObj2) // true
deepEqual(shallowObj1, shallowObj3) // false

That's it. Curious how it performs vs Lodash (maybe my next post should be figuring out how to do isolated function benchmarks 🤔), but it seems to work well in these few generic tests.

Update: As it turns out my implementation is about 35% slower than Lodash's. 🤷‍♂️ Ah well, it was fun to tinker with.

Default Named Parameter Functions for Everything


pubDate: 2016-10-11

image

I'm starting to wonder if we should just use named params for everything. They provide a lot of flexibility with no side-effects that I know of.

Here's how you do it:

const foo = ({a = 1, b = 2}) => console.log(`a: ${a}\nb: ${b}`)

foo({})
// a: 1
// b: 2

foo({b: 3})
// a: 1
// b: 3
  • We don't have to pass any smelly stuff like foo(undefined, 3).
  • Our parameter is more descriptive.
  • Our parameter is more flexible because it's not bound to any order-of-appearance.

Equal sign when setting defaults. Colon to assign when calling.

Yes, now you have to at least pass an empty object to your functions, and yes, we're forcing people to pass objects at all times, but there's probably an argument to be made about objects being more flexible anyway, and all-in-all, these are small prices to pay for how nice default arguments and named params are.

Named params have always been one of my favorite features of a language. JS doesn't seem to support a super-clean (still have to pass these as objects) way to do this, but at least now we can do it somewhat elegantly.

Now... if only there was a way to type-check those params...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.