nzakas/proposal-write-once-const

Showcase stronger examples

Closed this issue ยท 6 comments

Hi! Thanks for drafting this proposal. I certainly feel the pain behind every example provided in the README. However, I must admit all of them can be solved and in a more elegant way than creating a write-once const. Allow me to demonstrate.

Example 1

let value;

if (someCondition) {
  value = 1;
} else if (someOtherCondition) {
  value = 2;
} else {
  value = 3;
}

This isn't really an assignment of value but computing of value. There's a logic to that computing (conditionals), and so it belongs to a function:

function computeValue() {
  if (someCondition) return 1
  if (someOtherCondition) return 2
  return 3
}
const value = computeValue()

This is a great pattern that makes the computation explicit, testable in isolation (and you do need to test computeValue!), and keeps the original assignment to value simple while still using const.

Example 2

let value1, value2;

if (someCondition) {
  value1 = 1;
  value2 = 5;
} else if (someOtherCondition) {
  value1 = 2;
  value2 = 10;
} else {
  value1 = 3;
  value2 = 15;
}

This should be solved by moving out the computation for value1 and value2 as well. However, since the emphasis of this example is in multiple values, and those values are assigned based on shared conditions, this strongly suggests that a choice of a better data structure should be considered.

function computeValue() {
  if (someCondition) {
    return { a: 1, b: 5 }
  }

  if (someOtherCondition) {
    return { a: 2, b: 10 }
  }

  return { a: 3, b: 15 }
}
const value = computeValue()
// value.a / value.b

Collocation on the data structure level is generally beneficial but you can disregard this suggestion if the relevance of value1 and value2 is purely coincidental. If it is, then the same suggestion from Example 1 applies without fault to this one.

Example 3

async function doSomething() {
  let result;

  try {
      result = await someOperationThatMightFail();
  } catch (error) {
      return;
  }

  doSomethingWith(result);
}

This is a common pitfall when handling async/await. And yet I don't think it has to be solved on the const level.

Take a look at my approach to this problem in until:

const [error, value] = await until(() => someOperationThatMightFail())

I'd argue this example is out of scope of your proposal as it involves not only the value assignment but also error handling, which you can neither disregard nor accommodate within the current proposal.

If anything, this hints at a different proposal to help wrap async/await operations in something like until. A Promise.until(), perhaps?

Concerns

In general, I see the proposal to the change of the const behavior to be detrimental to the expectations and the benefits which the const was designed to provide.

Please note that per original intention, const is only ever meant to be used for module-wide constants. It never stood for an immutable version of var, but it often gets misused that way. Overall, let is preferred for variable assignments, and it already behaves as you are proposing.

TLDR const for top level module-scope constants only, and SCREAMING_CASE only. Otherwise let and call it a day.
โ€” Source

I know this is somewhat controversial but if the folks behind the spec say we are misusing const then, perhaps, we are misusing const. Suggesting additional, branching behaviors to it will not make things easier.

Thanks for the feedback. Indeed, I intentionally did not show refactors using functions because it's pretty clear that most developers are not doing that, otherwise I wouldn't be able to find examples of the patterns I mention. (Finding these patterns in the ESLint source code is what led me to write up this proposal.) I added a new FAQ to cover this because you're the second person to ask about it.

Please note that per original intention, const is only ever meant to be used for module-wide constants. It never stood for an immutable version of var, but it often gets misused that way. Overall, let is preferred for variable assignments, and it already behaves as you are proposing.

Through ESLint, I've been watching how people are actually using const and let for nine years now. Very shortly after ES2015 was released, people started using const not just for module-wide constants, but for any immutable bindings. That's why prefer-const is such a popular ESLint rule. At this point, I think it's fair to say that const is a general-purpose immutable binding.

Your point about getting better examples is a good one. I think it's important for the explainer to show simple examples that illustrate the usage patterns and then link off to more complex, real-world examples (as I did with the try-catch use case). I'm continuing to look for those real-world examples, but there's only so many hours in the day. ๐Ÿ˜„

I also profoundly disagree that const was only "intended" for module-level constants - it was intended for any binding that can't be reassigned, which is how it's used.

@ljharb, I believe I'm quoting one of the folks who worked on the spec. I agree with you, in practice it's used completely differently. Keeping the specs in-line with reality is a commendable effort, and if this proposal makes it so, it would be good.

That's just one person, 2+ years after the feature was shipped in the spec. That does not reflect the intent of the committee, only the opinion of one person.

@nzakas just to offer another voice here, I saw your examples and thought they were great and immediately understandable and following much of code in the wild ๐Ÿ‘

More examples are always good, but the existing ones are very good motivators in my opinion.

Thanks @karlhorky ๐Ÿ™