Overzealous Destructuring
aleksandrhovhannisyan.com
aleksandrhovhannisyan.com
Some developers seemingly can't live in a world where certain constructs can be used sometimes in the code, and others at other times, depending what is more appropriate in a given context. If they think construct B is superior over A, they will put in place an eslint rule that forbids A and always forces you to use B.
In some cases it happens that B is clearly superior because A is known to be broken for reasons x,y,z, then I'm okay with this.
But sometimes those kind of rules are taken out of nowhere for no good reason, just a personal preference, or with a fake reason like this blog shows (certain construct is only better at certain times, but not always).
I will say it's a little weird the ways in which some people think they've found a 'gotcha' and instead what's happened is that they're thinking 3 moves ahead and you're thinking 6.
ETA: What happens in plants is that I can walk down a street, and see dying trees that the owners think are healthy, and mostly healthy trees that the owner thinks are dying (when just a few strategic cuts would give them another 30 years). Sometimes you can read the sad story of decline in the dying ones, a repeating cycle of waiting too long to do preventative care, and overreaction leaving scars that never fully heal. Getting either tree owner to act is often like talking to a wall. Basically the same problem happens in software.
You can never defeat the weeds; so you have to be vigilant and pull the early before they drop seed.
I find the same practice also works well in code: consistent "weed pulling" to keep things tidy.
But even the more innocuous ones can eventually block access to things, or become a hazard, and you have to clear them from the main areas in order to get anything done, or to see the nastier weeds hiding behind or under them.
There is a lot of unquestioned dogma going around in this industry.
no wonder their website is so slow!
These rules and tools go in to fix a hurt caused by making mistakes, and somewhere it gets twisted away from "not making the same dumb mistakes repeatedly" to "never make any mistakes", which slows things down until you eventually make the chiefest of mistakes for a capitalist venture: never shipping.
Two of my better bosses pointed out that only people who risk nothing avoid making mistakes. One of those thought that "perfect estimates" meant you were over as often as you were under (the old accuracy vs precision argument). You can't achieve anything without taking some risks, and 'boring company' only works in a few niche markets, that don't pay well.
> These rules and tools go in to fix a hurt caused by making mistakes, and somewhere it gets twisted away from "not making the same dumb mistakes repeatedly" to "never make any mistakes" (except for the chiefest of mistakes for a capitalist venture: never shipping).
tools cant really prevent mistakes anyways, because people will always find a way to "get the job done anyway" by working around them... the end result in many cases is actually... more mistakes!at some point you have to hire people you can trust, no tool or process can fix that!
I guess you need to teach people like that how they can work with the tools to get things done, with higher quality results.
For my part I very much appreciate that ESLint and TypeScript can help me write better code, they're helpful and consistent reminders. I understand that I should use ===, I understand I should check for null, but without the tools I won't remember to do it in 100% of the cases I should.
Destructuring in Clojure doesn't suffer from the same problems as JavaScript, so lot of the things in the article doesn't apply. I was always slightly disappointed when destructuring finally came to JavaScript but it was so neutered compared to what it could have been.
But on the other hand, I'm happy JavaScript is not becoming more complicated just for the sake of new features. But on the other foot, we already got `class` and a bunch of other complicated, syntactic sugar, so why not another?
(defn foo [{:keys [a b c] :as bar}]
[bar a b c])
(foo {:a 1 :b 2 :c 3}) ;; => [{:a 1, :b 2, :c 3} 1 2 3]
in JS you can do const f = ({a,b,c}) => [???, a, b, c]
but how do you bind to the whole object as well? (defn foo [{a 1
b 2 :as bar}]
[bar a b])
(foo {1 "hi" 2 "world" 3 "!"}) ;; => [{1 "hi", 2 "world", 3 "!"} "hi" "world"]
In JS of course you can have Maps with non-string keys, but destructuring doesn't work on them. Plus you have other issues like arrays in JS having equality defined by reference rather than value, making maps less useful overall. let foo = (bar) => {
let {a, b, c} = bar
return [bar, a, b, c]
}
foo({ a: 1, b: 2, c: 3 }) // => [ { a: 1, b: 2, c: 3 }, 1, 2, 3 ]
Though JavaScript object keys are strictly strings and symbols, you can destructure dynamic keys like so: let foo = (bar) => {
let {[1]: a, [2]: b} = bar
return [bar, a, b]
}
foo({ 1: "hi", 2: "world", 3: "!" }) // => [ { '1': 'hi', , '2': 'world', '3': '!' }, 'hi' 'world' ]Optional chaining or null checks should not be used "defensively". They should just be used whenever TypeScript tells you to. Otherwise you just exacerbate the problem and end up with more unanticipated null values.
If you're dealing with an API with untrustworthy types or lots of null values, the solution is to:
a) Write mostly optional type definitions for that API, and/or
b) Use zod to verify the API data before you use it, to make sure it matches your expectations.
Agreed. I often find the `Uncaught TypeError: Cannot read properties of null` error annoying. But I would rather see the error than let unknown nulls sneak through my codebase.
1. Wait until TS complains about a possible null
2. If you have a good default value, use that instead
3. If not, throw a meaningful error
Either way, don't propagate the nulls further than they need to go.
The modern web experience really is subpar.
The real benefit of destructuring isn't really brevity, per se, IMO, but easy extensibility. You all of a sudden need two or three properties from the same object at the same level? You just reference the additional properties on the left side without any fuss.
const Component = (props) => { funcA(props.a.deeply.nested.variable); funcB(); funcC(props.a.deeply.nested.variable); };
Depending on what the functions are doing, neither the VM nor tsc might be able to infer that variable's value stays the same or that props, props.a, props.a.deeply, or props.a.deeply.nested will stay the same for that matter.
The VM will likely have to generate code to dereference the chain all over for the second use, and the compiler might lose narrowing information. Both of these can easily be avoided with destructuring.
(You could use "const variable = props.a.deeply.nested.variable", but then you have many of the same issues the article complains about.)
IMHO the problem here is not the destructuring, it's passing a gigantic object to a function when the only important stuff are a pair of leaf nodes.
const Component = (props) => {
console.log(props.a.deeply.nested.variable);
};
In the article the author said that this is a good approach (and there is a second one filled with `?.` between each levelThis is a symptom of a bigger problem: what has happened to the Law of Demeter? I think we drop a lot of good practices, when we switched from OOP and "functions + data structures".
Imagine we have the two following objects:
const person = {name: "John"}
const book = {title: "Bible"}
Only one of the following functions tell the user (developer who uses the function) A) what is actually needed for the function to work by just looking at the function signature and B) works for both objects, regardless of where `name` comes from. function sayHello(person) {
console.log(`Hello ${person.name}`)
}
function sayHello(name) {
console.log(`Hello ${name}`)
}
Not to mention, passing smaller things leads to less memory usage, which is generally also a good idea.It's not using any more memory. Whether you pass in the person object or the string, you're putting a reference on the stack. There will be a slight speedup with the more generic one if you are accessing the name more than once in the function, but you can avoid that by storing the name in a local variable.
And picking between the two isn't as straightforward as you make it out. Sure, you make it so any string can go into sayHello, but now you've introduced repetition at the call sites as they each have to pick the correct property to pass in. Also it means you can't easily change the sayHello function to do something like meeting a new requirement that it needs to greet the user more formally. If I take the whole person object, the change can be made in a single spot instead of having to modify the callers:
function sayHello(person) {
console.log(`Hello ${person.title} ${person.lastName}`);
}Destructuring is guaranteed to be 100% safe if you do it properly. That's the whole point.
const envelope = {data:{edge:{node:{value:'test'}}}}
const {data: {edge: {node: {value = ''} = {} } = {} } = {} } = envelope || {}
console.log(value)
Is completely safe. Versus the alternative of using an accessor library or null checking at each level.You can also do it with arrays to avoid out of range errors:
const arrayVals = ['test']
const {0: firstEl = ''} = arrayVals || []
console.log(firstEl)
100% safe and avoids mutating the array