Hacker News new | ask | show | jobs
by achou 2410 days ago
I just did some refactoring on a medium size code base and here are a few things to watch out for when adopting optional chaining and the new null coalescing operator:

  foo && await foo();
is not the same as

  await foo?.();
this will work in most cases but subtly, the await wraps the undefined case into a Promise, while the original code would skip the await altogether.

String regular expression matching returns null, not undefined, so rewriting code such as:

  const match = str.match(/reg(ex)/);
  return match && match[1];
is not the same thing as:

  return match?.[1];
because the latter returns undefined, not null, in case of match failure. This can cause problems if subsequent code expects null for match failure. An equivalent rewrite would be:

  return match?.[1] ?? null;
which is longer than the original and arguably less clear.

A common idiom to catch and ignore exceptions can interact poorly with optional chaining:

  const v = await foo().catch(_ => {});
  return v?.field; // property 'field' does not exist on type 'void'
This can be easily remedied by changing the first line to:

  const v = await foo().catch(_ => undefined);
Of course, these new operators are very welcome and will greatly simplify and help increase the safety of much existing code. But as in all things syntax, being judicious about usage of these operators is important to maximize clarity.
2 comments

&& ?. || ??

It's a shame JS at the beginning doubled down on the "billon dollar mistake" [1] with two(!) kinds of NULL instead of just using Maybe/Option.

Ah well, if it were good it wouldn't be popular :/

[1] https://www.lucidchart.com/techblog/2015/08/31/the-worst-mis...

Going between rust and ts, it's the judicious use of enums that js really feels like it's missing. The whole hoopla with undefined/null/NaN/etc could be avoided with a simple enum type. Not to mention the entire concept of Exceptions.

Nullish alone had me back to the == instead of === once I went ts. No reason to care about identity when ts makes sure I don't compare a string to a number, but treating undefined == null as true is what I want 99.9% of the time, and that 0.1% I should be explicit that I do care about treating them differently.

Sorry, JS? While JS might get this stuff one day, these language features are for TypeScript which is its own language. It's strongly typed and just happens to interop with and in some scenarios transpile down to JavaScript. It's whole existence is to deal with that billion dollar mistake you mentioned.

Speaking of which, optional chaining and null coalescence are core language features of some very good languages. Kotlin and C# for instance. Kotlin, much like TypeScript, interops with a "broken" language (Java and the JVM in its case) and attempts to address some core deficiencies in that ecosystem. Let us have nice things!! :)

I am really excited about these new features and hope they do land in JS sooner rather than later. I hope they do the pipe operator next! `pipe |> operator|> plz`

> TypeScript which is its own language. It's strongly typed and just happens to interop with and in some scenarios transpile down to JavaScript. It's whole existence is to deal with that billion dollar mistake you mentioned.

I'm afraid literally everything in this snippet is incorrect. The Typescript website opens with:

> Typescript JavaScript that scales. Typescript is a typed superset of JavaScript that compiles to plain JavaScript.

Typescript is Javascript. It's a superset, and as such its improvements are additive-only, by definition. The purpose of its existence is not to change or replace any JS features, only to augment them.

> Typescript is Javascript.

In the same way as an Animal is a Camel.

In the same was as programming languages are not organisms.

Typescript is ECMAScript in the same way as Netscape JavaScript is ECMAScript, in the same way as ActionScript is JavaScript.

In the same was as Scheme is Lisp.

In a similar but not identical way as American English is English (similar but not identical since programming languages and spoken languages are both languages, but not actually the same thing: the analogy cannot map perfectly).

If you're making general statements about animals, then they should also apply to a set of camels.
> optional chaining and null coalescence are core language features of some very good languages

They are features of Maybe/Option too, just in a more consistent extensible way.

    val a = Some(thing)
    val b = a.flatMap(_.part).flatMap(_.subpart)

    val c = Some(None) // look Ma! a nested option!
> While JS might get this stuff one day

I appreciate the attempt at pedantry, but TypeScript general only implements JS language features or TC39 Stage 3 proposals.

> I hope they do the pipe operator next!

Great example. After many hundreds of upvotes, the status is "waiting for TC39". [1] I.e. it will not be implemented until JS has it or is close to having it.

TypeScript is typed JavaScript, end of story.

[1] https://github.com/Microsoft/TypeScript/issues/17718

Show me where decorators are in the ecmascript standard.

How about private variables in classes? (Hint: they're different)

Typescript is a superset not just typed JavaScript.

I had actually listed decorators as a rare counterexample, but then removed it from my comment for brevity.

1. Decorators are currently TC39 Stage 2. [1]

2. Because of its unusual status, TypeScript lists decorators as "experimental, subject to change". Once the EcmaScript proposal advances, TS will ensure it is compatible with the ES standard and then remove the experimental sticker.

3. If it doesn't affect runtime, then you shouldn't expect JS to have the feature. TS private members are no different than public members at runtime; there is no need for JS to have that feature. (Note that TypeScript deliberately chooses not to name-mangle members.)

4. Sure call it a "feature and syntactic superset", but that doesn't make TS any less beholden to JS. Dedicated adherence to that property commits them to support every feature and syntax that JS adds; future-proofing means they can't really add anything that JS doesn't have or is going to have.

5. Note that TS abandoned having a standard long ago. The behavior and validity of TypeScript program is determined by "whatever tsc does" and the ES standard.

6. Again, if you need convincing that TS effectively only implements JS features, refer the linked pipeline issue, locked as "waiting for TC39."

[1] https://github.com/tc39/proposal-decorators

> just happens to interop with

It does not interop with JavaScript.

> and in some scenarios transpile down to JavaScript

It always transpiles to JavaScript and always runs as JavaScript. There is no such thing as a TypeScript runtime engine.

TypeScript is a superset of JavaScript. Therefore the OP's point is still valid. Any "mistakes" JavaScript might have made about having null AND undefined are also issues for TypeScript.

> It does not interop with JavaScript.

I don't know what you mean here, but it is certainly possible for TypeScript code to use JavaScript libraries and vice versa, which is presumably what most people mean by "TypeScript interops with JavaScript".

> It always transpiles to JavaScript and always runs as JavaScript.

Technically false... https://assemblyscript.org

> TypeScript is a superset of JavaScript. Therefore the OP's point is still valid. Any "mistakes" JavaScript might have made about having null AND undefined are also issues for TypeScript.

TypeScript adds type checking to JavaScript. The Million Dollar Mistake is having unchecked nulls; TypeScript supports checked nulls so it's not an issue. TypeScript's nulls are much more similar to Maybe/Option than unchecked nulls.

> I don't know what you mean here, but it is certainly possible for TypeScript code to use JavaScript libraries and vice versa, which is presumably what most people mean by "TypeScript interops with JavaScript".

Ah, I can see what you/they mean by that. The point I was trying to get across was: TypeScript doesn't exist when code is actually executing (which is what I think of as interop - it's happening at execution time.) At execution time - it's all just JavaScript.

I have found (working in a TypeScript team currently) that this fact is ignored, primarily by people who "look down" on JavaScript, but it is a VERY important point to remember when you are writing TypeScript, mostly because it's important to remember there is only compile time type checking not run time.

> Technically false... https://assemblyscript.org

Heh, yes - as soon as I posted I realised that was silly. The word "always" is almost "always" incorrect! I should have said: "It usually transpiles to JavaScript and usually runs as JavaScript"

> The Million Dollar Mistake is having unchecked nulls; TypeScript supports checked nulls so it's not an issue. TypeScript's nulls are much more similar to Maybe/Option than unchecked nulls

Good point in theory but my practical experience hasn't borne this out. That is because TypeScript is an "optionally typed" language and it hasn't been true in practice because of excessive use of explicit or implicit "any"s.

<i> > The Million Dollar Mistake is having unchecked nulls; TypeScript supports checked nulls so it's not an issue. TypeScript's nulls are much more similar to Maybe/Option than unchecked nulls</i>

<i>Good point in theory but my practical experience hasn't borne this out. That is because TypeScript is an "optionally typed" language and it hasn't been true in practice because of excessive use of explicit or implicit "any"s. </i>

I think that's a matter of your team's discipline. It's good practice, I think, to enable TypeScript's strict checks, including no-implicit-any, and, to the best of your ability, to keep people who don't understand types ignorant of explicit any and to fail any code that uses it. `any` is basically never necessary even in typing existing code - if you genuinely don't know what the type is at a certain point, you should probably write a type like `unknown`.

If you take any of Typescript's options to "ease the transition" you're taking Typescript's options to continue the difficulties. One moves to typescript because javascript's runtime errors are a problem; so it is natural that you will have novel compile time errors.

AssemblyScript is not TypeScript. Especially so if you don't consider TypeScript to be JavaScript; I'd argue that the difference between TypeScript and AssemblyScript is bigger than TypeScript and JavaScript.
This does not necessarily invalidate your wider points, but just FYI:

> It does not interop with JavaScript.

Hm, this depends on your definition of "interop". My JavaScript and TypeScript are languages that exchange information. The execution model ultimately involves JavaScript when I use tsc, but ultimately it also includes an interpreter. My user-space syntax doesn't care.

> There is no such thing as a TypeScript runtime engine.

Not being glib, just a heaps up: https://github.com/denoland/deno

>Not being glib, just a heaps up: https://github.com/denoland/deno

interesting thanks for the heads up - my assumption is though that this is just a wrapper over a V8 engine with typescript compilation on the fly?

Does that make it a TypeScript runtime or not?

V8 is a JavaScript runtime that compiles JavaScript on the fly, so it seems reasonable to say that a runtime that compiles TypeScript on the fly is a TypeScript runtime.
Can you really consider Deno to be a "TypeScript runtime engine" when it compiles TypeScript to JavaScript and runs it in V8, a JavaScript engine.
These features are copied from TC39 proposals for JavaScript. Presumably they were deemed safe to add to TS now that they have reached Stage 3 as JS proposals.

https://github.com/tc39/proposals

Not so much 'copied', considering it was the TS team that has gotten them added to JS. They just didn't want to commit to anything in the TS codebase that wasn't at least stage 3 in JS.
Out of curiosity, have there been features that were thought to be part of JS one day, added by TypeScript, and then abandoned for JS again, leading TS to rip them out as well? Or do such vestiges remain in TypeScript and have to be changed into different constructs on compilation?
TypeScript has incompatibilities with the private keyword and decorators. I don't think they have a strategy to deprecate.
I don't see it as two kinds of null, there is a null value, and then there is the fact that no value has ever been defined, which is undefined.

It can be useful to have the latter case distinguished in a dynamic language because it can enable certain powerful patterns. At the end of the day, compressing both these cases to a single concept of null would be lossy. This may have certain advantageous implications for simplicity, but you're trading that off for language power. Which you favour more of course depends on the usecase.

Or, use Maybe/Option and have as many levels of nonexistence as you want.

    Option[Option[Option[T]]]
Not that it would necessarily be very useful, but choosing 2 values of nonexistence is very arbitrary.

Usually zero or one level of nonexistence is enough.

There is yet a third type of missing value in JS: empty array slot. This appears when you create an array with a length but no values for indexes in that length, e.g. `new Array(100)`.

Edit: also `undefined` is a value in JS as well.

You have to watch out for first and last one in JavaScript but not on TypeScript as it isn't possible to make that mistake because you have to type it as a promise or in the last one as void.

You can even avoid the problem in the second one by using NonNullable TypeScript types, but I admit that's not common so its still likely to arise.

The first example can happen in TypeScript; foo has type

  (() => Promise<void>) | undefined
admittedly it may not be all that common to have a function-valued variable that may be undefined, but it happened in the code base I was working with.

In the last example, you're right that TypeScript will catch this at compile time. My point was to show how this compile time error can happen from refactoring to use optional chaining, and one easy solution in this case.