I get why we prefer final in languages with a need for thread safety, but I have never understood why people prefer const in typescript. I have seen people bend over backwards to avoid a `let` even if it results in higher complexity code. It just doesn’t seem to solve a problem I’ve ever actually encountered in typescript code.
The only reason to use let is to indicate to the reader that you're going to be reassigning the variable to a new value at some point within the current scope.
If you aren't going to be doing that, using const lets the reader know the assignment won't change to refer to a new value.
Yes, I've heard this reason before, but how does that help? The only benefit of that particular arrangement as far as I can tell is that it's possible to enforce via linter. It doesn't signal intent, it merely states a fact that could be observed by reading the source code. What is the point of that? What problem does it solve? Are people running into hard to debug issues because a variable got reassigned? I don't remember that ever causing an issue before the advent of const. People go to great lengths to avoid writing `let` just because the linters have been telling them to use `const` so they consider `let` a code smell now. Is this really better?
It tells you up front what you would otherwise have to go line by line to find out.
There's a similar argument to be made for using forEach/ map / reduce / filter instead of a for loop. Each of those iterative methods has slightly different semantic intent. The only thing the for loop adds to the mix, and therefore the only reason to use them, is the availability of continue / break.
Hell, the same argument could be made for while loops vs for loops. What does a for loop really add that you couldn't get with a while loop?
As for your last point, I don't know of any linter that complains about the use of let when you reassign it. The ones I've used only complain if you use let when const would have worked identically in your code.
The fact that people write bad code is not down to let/const or linters. People always have and always will write crap and not understand why.
Assuming assert existed, it would almost certainly be judging its value for being falsy, while the ?? operator judges its LHS for being nullish, which is a narrower category. For strings, this affects whether the empty string is acceptable or not.
Fair, this is a good example of when != is actually the right choice (instead of !==). However, on web browsers, this will log an assertion failure to the console, but it will not throw an exception. It's also not suitable for the original context, where the non-null value was extracted into a variable.
I keep wondering about a type system where you can say something like "A number greater than 4" or "A string of length greater than 0" or "A number greater than the value of $othernum". If you could do that, you could push so much of this "coping" logic to only the very edge of your application that validates inputs, and then proceed with lovely typesafe values.
There is some ceremony around it, but when you do the basic plumbing it's invaluable to import NonEmptyString100 schema to define a string between 1 and 100 chars, and have parsing and error handling for free anywhere, from your APIs to your forms.
This also implies that you cannot pass any string to an API expecting NonEmptyString100, it has to be that exact thing.
Or in e-commerce where we have complex pricing formulas (items like showers that need to be custom built for the customer) need to be configured and priced with some very complex formulas, often market dependent, and types just sing and avoid you multiplying a generic number (which will need to be a positive € schema) with a swiss VAT rate or to do really any operation if the API requires the branded version.
Typescript is an incredibly powerful language, it is kinda limited by its verbose syntax and JS compatibility but the things that you can express in Typescript I haven't seen in any other language.
while this is nice, the type itself doesn't encode the logic (unlike refinement type)
i think this would be really nice if validation libraries like zod returned branded types when they are validating non-comp-time types (like z.ipv4() should return some IPv4 branded type)
The type encodes the logic in the schema, it is absolutely a refinement as every parser is. Maybe you meant a comparison with dependent types?
Now every time you will have to use a NonEmptyString255 as a type it has to be branded by passing through the constructor, so you can't pass a normal string to an API expecting it, and you get the error at type level. The logic is encoded in the schema itself, which you can click.
And it also provided the decoder (parser) and encoder (constructor). So you use the parser in a form or whatever and get parsing and precise errors (for it being too long, too short, not a string). And you can annotate the errors in any language you want too (German, Spanish, etc, English is the default)
Essentially this approach is similar to using some class NonEmptyString without using a class and while keeping the information at type level.
It's practical and the ceremony goes as far as copy pasting or providing a different refinement, besides, AI can write those with ease and you don't need to do it frequently, but it's nice in many places not mixing UserIDs with ProductID or any other string makes codebases much easier to follow and provides lots of invariants.
I'm not familiar with Zod, but one thing that is quite important on the user end is to produce multiple (but, per policy, not infinite) error messages before giving up. That is, list all environment variables that need to be set, not just whichever one the code happens to be first.
This could be implemented with `??`, something like: `process.env.NODE_ENV ?? deferred_error(/temporary fallback/'', 'NODE_ENV not set')`, but is probably best done via a dedicated wrapper.
Early errors are good, but I think the author overstates the importance of filtering out empty strings
---
I disagree that erroring out when the app doesn't have ALL the data is the best course of action. I imagine it depends a bit on the domain, but for most apps I've worked on it's better to show someone partial or empty data than an error.
Often the decision of what to do when the data is missing is best left up the the display component. "Don't show this div if string is empty", or show placeholder value.
---
Flip side, when writing an under the hood function, it often doesn't matter if the data is empty or not.
If I'm aggregating a list of fooBars, do I care if fooBarDisplayName is empty or not? When I'm writing tests and building test-data, needing to add a value for each string field is cumbersome.
Sometimes a field really really matters and should throw (an empty string ID is bad), but those are the exception and not the rule.
It seems Rust's unwrap is the exact opposite of ?? "". It throws an error instead of using a fallback value, which is exactly what the author suggests instead of using ?? "".
> In Rust, however, you're forced to reason about the "seriousness" of calling .unwrap() as it could terminate your program. In TS you're not faced with the same consequences.
Also unwrap_or_default() which is useful in many cases. For example the default for a string is empty string, default for integers is 0 and default for bool is false.
For your own types you can implement the Default trait to tell Rust what the default value is for that type.
The problem I have when writing JS/TS is that asserting non-null adds verbosity:
Consider the author’s proposed alternative to ‘process.env.MY_ENV_VAR ?? “”’:
if (process.env.MY_ENV_VAR === undefined) {
throw new Error("MY_ENV_VAR is not set")
}
That’s 3 lines and can’t be in expression position.
There’s a terser way: define an ‘unwrap’ function, used like ‘unwrap(process.env.MY_ENV_VAR)’. But it’s still verbose and doesn’t compose (Rust’s ‘unwrap’ is also criticized for its verbosity).
TypeScript’s ‘!’ would work, except that operator assumes the type is non-null, i.e. it’s (yet another TypeScript coercion that) isn’t actually checked at runtime so is discouraged. Swift and Kotlin even have a similar operator that is checked. At least it’s just syntax so easy to lint…
> Personally, I've come to see this ubiquitous string of symbols, ?? "", as the JS equivalent to .unwrap() in Rust
It's funny you bring this up because people opposed to `.unwrap()` usually mention methods like `.unwrap_or` as a "better" alternative, and that's exactly the equivalent of `??` in Rust.
Semantically the two are kind of opposite; the similarity is that they're the lowest-syntax way in their respective languages to ignore the possibility of a missing value, and so get overused in situations where that possibility should not be ignored, leading to bugs.
Is this a weakness in the type definition? If we're sure the value cannot be undefined, then why doesn't the type reflect that? Why not cast to a non-undefined type as soon as we're sure (and throw if it is not)? At least that would document our belief in the value state.
If your type definitions are airtight then this problem doesn't come up, but sometimes, for whatever reason which may not be entirely within your control, they aren't. Narrowing and failing early is precisely what the article advises doing.
The user is an entity from the DB, where the email field is nullable, which makes perfect sense. The input component only accepts a string for the defaultValue prop. So you coalesce the possible null to a string.
The issue isn't the nullish coalescing, but trying to treat a `string | null` as a string and just giving it a non-sense value you hope you'll never use.
You could have the same issue with `if` or `user?.name!`.
Basically seems the issue is the `""`. Maybe it can be `null` and it should be `NA`, or maybe it shouldn't be `null` and should have been handled upstream.
This is a fine line. If you get a white screen of death you know something is wrong. If the first name is missing it may mean other things are missing and the app is in a bad state. That means the user could lose any work they try to do, which is a cardinal sin for an app to commit.
Context matters a lot. If it's Spotify and my music won't play is a lot different than I filled a form to send my rent check and it silently failed.
If the error isn’t repairable by the user, blocking them from using the app entirely is mean. If the error screen has a message telling the user where to go to set their name, that’s fine but annoying. If the error screen tells the user they can’t use the app until someone checks a dashboard and sees a large enough increase in errors to investigate, that’s a bigger problem.
This reads like a dogmatic view of someone who hasn’t worked on a project that’s a million plus lines of code where something is always going wrong, and crashing the entire program when that’s the case is simply unacceptable.
tldr: undefined constants were treated as a string (+ a warning), so `$x = FOO` was `$x = "FOO"` + a warning if `FOO` was not a defined constant. Thankfully this feature was removed in PHP 8.
It’s a feature of the language that’s totally fine to use as much as needed. it’s not a quick n dirty fix.
Don’t listen to those opinionated purists that don’t like certain styles for some reason (probably because they didn’t have that operator when growing up and think it’s a hack)
This post and everyone defending this is absolutely crazy
Is this a kink? You like being on call?
You think your performance review is going to be better because you fixed a fire over a holiday weekend or will it be worse when the post mortem said it crashed because you didn’t handle the event bus failing to return firstName
Should throw expressions (https://github.com/tc39/proposal-throw-expressions) ever make it into the JavaScript standard, the example could be simplified to:
so like assert(process.env.MY_ENV_VAR) but in a less readable oneliner?
Since JS doesn’t have if statements return values, null chaining is a great way to keep both const variables and have some level of decidability.
Null chaining is also a common pattern across most languages and is generally seen as readable
I get why we prefer final in languages with a need for thread safety, but I have never understood why people prefer const in typescript. I have seen people bend over backwards to avoid a `let` even if it results in higher complexity code. It just doesn’t seem to solve a problem I’ve ever actually encountered in typescript code.
The only reason to use let is to indicate to the reader that you're going to be reassigning the variable to a new value at some point within the current scope.
If you aren't going to be doing that, using const lets the reader know the assignment won't change to refer to a new value.
It's that simple.
Yes, I've heard this reason before, but how does that help? The only benefit of that particular arrangement as far as I can tell is that it's possible to enforce via linter. It doesn't signal intent, it merely states a fact that could be observed by reading the source code. What is the point of that? What problem does it solve? Are people running into hard to debug issues because a variable got reassigned? I don't remember that ever causing an issue before the advent of const. People go to great lengths to avoid writing `let` just because the linters have been telling them to use `const` so they consider `let` a code smell now. Is this really better?
It tells you up front what you would otherwise have to go line by line to find out.
There's a similar argument to be made for using forEach/ map / reduce / filter instead of a for loop. Each of those iterative methods has slightly different semantic intent. The only thing the for loop adds to the mix, and therefore the only reason to use them, is the availability of continue / break.
Hell, the same argument could be made for while loops vs for loops. What does a for loop really add that you couldn't get with a while loop?
As for your last point, I don't know of any linter that complains about the use of let when you reassign it. The ones I've used only complain if you use let when const would have worked identically in your code.
The fact that people write bad code is not down to let/const or linters. People always have and always will write crap and not understand why.
Assuming assert existed, it would almost certainly be judging its value for being falsy, while the ?? operator judges its LHS for being nullish, which is a narrower category. For strings, this affects whether the empty string is acceptable or not.
> Assuming assert existed
It's a function you could write for yourself and give it whatever semantics you feel is best. No changes to the language required for this one.
Fair, this is a good example of when != is actually the right choice (instead of !==). However, on web browsers, this will log an assertion failure to the console, but it will not throw an exception. It's also not suitable for the original context, where the non-null value was extracted into a variable.
There isn't a built-in assert function that behaves that way; you would need to either write it or import it.
I'm not a web dev. Are people really that opposed to writing a function which would solve problems in their project? It's two lines long...
That doesn’t assign it to the shorthand local variable.
It could return the given value if it doesn't throw, though, which would make using it with assignment trivial.
I keep wondering about a type system where you can say something like "A number greater than 4" or "A string of length greater than 0" or "A number greater than the value of $othernum". If you could do that, you could push so much of this "coping" logic to only the very edge of your application that validates inputs, and then proceed with lovely typesafe values.
You can do it in typescript with branded types:
https://effect.website/docs/schema/advanced-usage/#branded-t...
There is some ceremony around it, but when you do the basic plumbing it's invaluable to import NonEmptyString100 schema to define a string between 1 and 100 chars, and have parsing and error handling for free anywhere, from your APIs to your forms.
This also implies that you cannot pass any string to an API expecting NonEmptyString100, it has to be that exact thing.
Or in e-commerce where we have complex pricing formulas (items like showers that need to be custom built for the customer) need to be configured and priced with some very complex formulas, often market dependent, and types just sing and avoid you multiplying a generic number (which will need to be a positive € schema) with a swiss VAT rate or to do really any operation if the API requires the branded version.
Typescript is an incredibly powerful language, it is kinda limited by its verbose syntax and JS compatibility but the things that you can express in Typescript I haven't seen in any other language.
while this is nice, the type itself doesn't encode the logic (unlike refinement type)
i think this would be really nice if validation libraries like zod returned branded types when they are validating non-comp-time types (like z.ipv4() should return some IPv4 branded type)
The type encodes the logic in the schema, it is absolutely a refinement as every parser is. Maybe you meant a comparison with dependent types?
Now every time you will have to use a NonEmptyString255 as a type it has to be branded by passing through the constructor, so you can't pass a normal string to an API expecting it, and you get the error at type level. The logic is encoded in the schema itself, which you can click.
And it also provided the decoder (parser) and encoder (constructor). So you use the parser in a form or whatever and get parsing and precise errors (for it being too long, too short, not a string). And you can annotate the errors in any language you want too (German, Spanish, etc, English is the default)
Essentially this approach is similar to using some class NonEmptyString without using a class and while keeping the information at type level.
It's practical and the ceremony goes as far as copy pasting or providing a different refinement, besides, AI can write those with ease and you don't need to do it frequently, but it's nice in many places not mixing UserIDs with ProductID or any other string makes codebases much easier to follow and provides lots of invariants.
https://en.wikipedia.org/wiki/Refinement_type
Thank you - I didn't know it had a name. But I'm not surprised it came from ML.
Your can do that now in Typescript. But it will take several minutes to resolve your types. I've done it and it's a horrible dev experience, sadly.
I can see this.
I learned from a friend to use Zod to check for process.env. I refined it a bit and got:
```
const EnvSchema = z.object({
});export type AlertDownEnv = z.infer<typeof EnvSchema>;
export function getEnvironments(env: Record<string, string>): AlertDownEnv { return EnvSchema.parse(env); }
```
Then you can:
```
const env = getEnvironments(process.env);
```
`env` will be fully typed!
Definitely, I need to do some improvements in my frontend logic!
I'm not familiar with Zod, but one thing that is quite important on the user end is to produce multiple (but, per policy, not infinite) error messages before giving up. That is, list all environment variables that need to be set, not just whichever one the code happens to be first.
This could be implemented with `??`, something like: `process.env.NODE_ENV ?? deferred_error(/temporary fallback/'', 'NODE_ENV not set')`, but is probably best done via a dedicated wrapper.
Yes use zod or equivalent.
I am quite surprised people here doesn't know how to validate data in runtime. The author completely mixing Typescript with runtime behavior.
a?.b?.c?.() or var ?? something have well documented use cases and it's not what the author is thinking.
Early errors are good, but I think the author overstates the importance of filtering out empty strings
---
I disagree that erroring out when the app doesn't have ALL the data is the best course of action. I imagine it depends a bit on the domain, but for most apps I've worked on it's better to show someone partial or empty data than an error.
Often the decision of what to do when the data is missing is best left up the the display component. "Don't show this div if string is empty", or show placeholder value.
---
Flip side, when writing an under the hood function, it often doesn't matter if the data is empty or not.
If I'm aggregating a list of fooBars, do I care if fooBarDisplayName is empty or not? When I'm writing tests and building test-data, needing to add a value for each string field is cumbersome.
Sometimes a field really really matters and should throw (an empty string ID is bad), but those are the exception and not the rule.
Sure, but in that case string | undefined is the correct type, and turning it into string | “” isn’t helping anybody.
It’s the difference between:
if (fooBarDisplayName) { show div }
And:
if (foobarDisplayName && foobarDisplayName.length > 0) { show div }
Ultimately, type systems aren’t telling you what types you have to use where - they’re giving you tools to define and constrain what types are ok.
It seems Rust's unwrap is the exact opposite of ?? "". It throws an error instead of using a fallback value, which is exactly what the author suggests instead of using ?? "".
From the article:
> In Rust, however, you're forced to reason about the "seriousness" of calling .unwrap() as it could terminate your program. In TS you're not faced with the same consequences.
Yes that was a mistake.
unwrap() is the error.
unwrap_or() is the fallback.
Also unwrap_or_default() which is useful in many cases. For example the default for a string is empty string, default for integers is 0 and default for bool is false.
For your own types you can implement the Default trait to tell Rust what the default value is for that type.
The problem I have when writing JS/TS is that asserting non-null adds verbosity:
Consider the author’s proposed alternative to ‘process.env.MY_ENV_VAR ?? “”’:
That’s 3 lines and can’t be in expression position.There’s a terser way: define an ‘unwrap’ function, used like ‘unwrap(process.env.MY_ENV_VAR)’. But it’s still verbose and doesn’t compose (Rust’s ‘unwrap’ is also criticized for its verbosity).
TypeScript’s ‘!’ would work, except that operator assumes the type is non-null, i.e. it’s (yet another TypeScript coercion that) isn’t actually checked at runtime so is discouraged. Swift and Kotlin even have a similar operator that is checked. At least it’s just syntax so easy to lint…
> Personally, I've come to see this ubiquitous string of symbols, ?? "", as the JS equivalent to .unwrap() in Rust
It's funny you bring this up because people opposed to `.unwrap()` usually mention methods like `.unwrap_or` as a "better" alternative, and that's exactly the equivalent of `??` in Rust.
Semantically the two are kind of opposite; the similarity is that they're the lowest-syntax way in their respective languages to ignore the possibility of a missing value, and so get overused in situations where that possibility should not be ignored, leading to bugs.
Is this a weakness in the type definition? If we're sure the value cannot be undefined, then why doesn't the type reflect that? Why not cast to a non-undefined type as soon as we're sure (and throw if it is not)? At least that would document our belief in the value state.
I may not understand.
If your type definitions are airtight then this problem doesn't come up, but sometimes, for whatever reason which may not be entirely within your control, they aren't. Narrowing and failing early is precisely what the article advises doing.
JavaScript isn't even statically typed.
> Why not cast to a non-undefined type as soon as we're sure (and throw if it is not)
This is exactly what the OP author suggests.
I use this operator all the time in a similar but not quite the same way:
<input type=“text” defaultValue={user.email ?? “”}>
The user is an entity from the DB, where the email field is nullable, which makes perfect sense. The input component only accepts a string for the defaultValue prop. So you coalesce the possible null to a string.
He lost me at the first example:
```ts user?.name ?? "" ```
The issue isn't the nullish coalescing, but trying to treat a `string | null` as a string and just giving it a non-sense value you hope you'll never use.
You could have the same issue with `if` or `user?.name!`.
Basically seems the issue is the `""`. Maybe it can be `null` and it should be `NA`, or maybe it shouldn't be `null` and should have been handled upstream.
In general, I agree. You don’t want silent failures. They’re awful and hard to reason about.
> By doing this, you're opening up for the possibility of showing a UI where the name is "". Is that really a valid state for the UI?
But as a user, if I get a white screen of death instead of your program saying “Hi, , you have 3 videos on your watchlist” I am going to flip out.
Programmers know this so they do that so that something irrelevant like my name doesn’t prevent me from actually streaming my movies.
This is a fine line. If you get a white screen of death you know something is wrong. If the first name is missing it may mean other things are missing and the app is in a bad state. That means the user could lose any work they try to do, which is a cardinal sin for an app to commit.
Context matters a lot. If it's Spotify and my music won't play is a lot different than I filled a form to send my rent check and it silently failed.
Totally agree, I think this is a good place for debug asserts which only throw during development, but fallback in prod builds.
> But as a user, if I get a white screen of death
No one suggests hard crashing the app in a way that makes the screen white. There are better ways. At least, send the error log to telemetry.
Well, what would be better is “Hi, you have…”, and that would require a conditional, not coalescing.
Exactly, something like a name missing shouldn't cause the app to completely error out for the user.
Yes it should, because hopefully errors are logged and reported and can be acted upon. Missing name doesn’t.
If the error isn’t repairable by the user, blocking them from using the app entirely is mean. If the error screen has a message telling the user where to go to set their name, that’s fine but annoying. If the error screen tells the user they can’t use the app until someone checks a dashboard and sees a large enough increase in errors to investigate, that’s a bigger problem.
This reads like a dogmatic view of someone who hasn’t worked on a project that’s a million plus lines of code where something is always going wrong, and crashing the entire program when that’s the case is simply unacceptable.
> something is always going wrong
I hate this sentence with a passion, yet it is so so true. Especially in distributed systems, gotta live with it.
Why not both?
That's how you get this feature: https://wiki.php.net/rfc/deprecate-bareword-strings.
tldr: undefined constants were treated as a string (+ a warning), so `$x = FOO` was `$x = "FOO"` + a warning if `FOO` was not a defined constant. Thankfully this feature was removed in PHP 8.
See also "Parse, don't validate (2019)" [0]
[0] https://news.ycombinator.com/item?id=41031585
It’s a feature of the language that’s totally fine to use as much as needed. it’s not a quick n dirty fix.
Don’t listen to those opinionated purists that don’t like certain styles for some reason (probably because they didn’t have that operator when growing up and think it’s a hack)
This post and everyone defending this is absolutely crazy
Is this a kink? You like being on call?
You think your performance review is going to be better because you fixed a fire over a holiday weekend or will it be worse when the post mortem said it crashed because you didn’t handle the event bus failing to return firstName