Hacker News new | ask | show | jobs
by itslennysfault 1196 days ago
hmm... I've been using JS/TS for almost as long as they've existed. A lot of these are nice. Some less so. Some quick thoughts:

- Tagged template strings. This just feels dirty to me. Probably won't use, but when I see it in a code base I won't be so confused at least

- matchAll. I've never needed this. I've used match with g a bunch, but I never need the capture group.

- Promise.allSettled. THIS is useful. I've implemented this (under a different name) in almost every code base I've worked on, and got bit HARD by not understanding this behavior long ago (huge production outage that took hours and many engineers to discover)

- globalThis. EW. Don't think I have to elaborate

- replaceAll. It's always annoyed me needing to use RegEx for simple replace all. so Yay!

- ??=, &&=, ||= These seem really useful, but also potentially hard to read, but I think if I get used to their existence they'd become second nature

- # private... not sure why they didn't just use the "private" keyword, but I don't care. I almost always use TypeScript anyways

- static ... YAY! finally. Again, if they could do this i don't see why not "private"

For the TypeScript stuff I'll just say the type system has kinda jumped the shark, but I don't hate it. It's SO robust and of all the new stuff being added I'll maybe use 1/10 of it, but it's good to know I can describe literally anything* with it if needed.

* EXCEPT IF I WANT TO USE AN ENUM/TYPE AS A KEY IN AN DICT WHICH I REALLY WANT TO DO!!

12 comments

> - Tagged template strings. This just feels dirty to me. Probably won't use, but when I see it in a code base I won't be so confused at least

Tagged template strings are an absolutely brilliant feature and have tons of valuable uses. In particular, many sql libraries in node let you do this:

    const query = sql`select foo from bar where zed = ${param}`;
From a developer standpoint it "feels" just like you're doing string concatenation, but in reality the query variable will contain a prepared statement so that it safely prevents any kind of SQL injection, e.g. it gets parsed to

    {
        sql: "select foo from bar where zed = ?",
        parameters: [param]
    }
There are lots of use cases where things are easily expressed as an interpolated string, but the thing you want back is NOT just a plain string, and tagged template literals are great for that. It's also a nice way to call a parser, e.g. many GraphQL libraries let you do:

    const parsedGraphQLSchema = gql`type Query { foo: Int }`;
Is it really an good thing that a vulnerable sql string interpolation code pattern and this sql tagged string look and feel the same?
Actually, yes, it is. The way these libraries work, since the thing that is parsed is NOT just a plain string, in most cases it's impossible to have sql injection without doing some deliberately nasty stuff. That is, you can't just do this:

    const query = `select foo from bar where zed = ${param}`; // forgot the sql tag
    await runQuery(query);
In that case, the type of query is just string, but the `runQuery` method doesn't take strings, it takes a parsed query, so that wouldn't work.

After using the tagged template literal pattern for SQL queries exclusively for the past couple years, I can't say enough how awesome it is to use in practice. Libraries even let you do strong typing with TypeScript to define the expected structure of the result, e.g.

    sql<MyExpectedReturnType>`select foo from bar where zed = ${param}`
> Libraries even let you do strong typing with TypeScript to define the expected structure of the result.

The tagged template does not return a string in this case?

No, it usually returns a parsed object. For example the gql tag in the apollo client libraries return a completely parsed query with all its various children and sub-objects.
I was thinking the same thing... If I do that I'm going to forget the "sql" part at least once and nothing's going to alert me about it.
The way libraries work it's impossible to forget the "sql" part and still have that query be executed - see my sibling comment.
Why wouldn’t it be? Do you think developers get inspired by this slick API and decide to write functions that talk directly to the database using unescaped interpolated strings? I doubt it.
I fear that syntactic sugar creates as many problems as it solves. For instance, one might wish to sort the results by whitelisted column:

    query = sql`select foo from bar where zed = ${p} order by ${col} asc`;
Unless the lib implements a real SQL parser for the right dialect, it will quote each expression in the same way, and will either fail or produce a broken SQL.
Definitely a lot of misconceptions around how this would work. Just check out something like slonik, https://github.com/gajus/slonik, which is an excellent implementation.

The example you gave actually isn't valid, because what you're doing is generating SQL dynamically, and that doesn't work the way prepared statements work. That is, you can't have a prepared statement like "select foo from bar where zed = ? order by ? asc", because with prepared statements the question marks can only substitute for VALUES, not schema names. So if you wanted to do something like that it slonik, it would fail. With slonik you CAN do dynamic SQL, that is guaranteed to be safe and checked at compile time with TypeScript, because you can nest SQL tagged templates. That is you can do this:

    const colToSortBy = useFoo ? sql`foo` : sql`bar`;
    const query = sql`select col from mytable order by ${colToSortBy}`;
In that case slonik will know how to safely "merge" the parent and child parsed SQL.
We actually did the same for ArangoDB (I think we first did this in 2015, I remember being surprised nobody had done something similar for SQL at the time). Here's the JS driver's current implementation of it:

https://github.com/arangodb/arangojs/blob/main/src/aql.ts#L1...

Basically the `aql` template tag returns an object that can also be fed back into it and we also deduplicate arguments to avoid sending redundant data over the wire. There's also an escape hatch via a helper function (`aql.literal`) in cases where you need to insert literals that aren't known at compile time (e.g. you load query filters from a configuration file).

> - # private... not sure why they didn't just use the "private" keyword, but I don't care. I almost always use TypeScript anyways

One of the reasons was to allow private and public fields of the same name, so that subclasses are free to add own public fields without accidentally discovering private fields of superclasses. There were many more considerations that went into the design: https://github.com/tc39/proposal-class-fields/blob/main/PRIV....

There was a heated debate about this and the choice of the # sigil back in 2015 at the time private fields were being designed: https://github.com/tc39/proposal-private-fields/issues/14.

>* EXCEPT IF I WANT TO USE AN ENUM/TYPE AS A KEY IN AN DICT WHICH I REALLY WANT TO DO!!

It's better just to use an actual array for enums:

myEnum = ["E1", "E2"...] as const

type myEnum = typeof myEnum[number]

That gets you both an enum type and an enum array you can use at runtime

I've used globalThis for polyfills in code that needs to run both in the browser and on node.
WRT Enum as key in object:

  enum TestEnum {
    Fizz = 0,
    Buzz,
    Bar,
    Baz
  }

  type EnumKeyedObject = Record<TestEnum, string>;

  type EnumKeyedObjectAlt = { [P in TestEnum]: string };
That's still awkward and confusing for what would be one of the most common use-cases, if it were less awkward.
How would you simplify the syntax here?
I'd like to be able to just do...

    type UserType = 'default' | 'admin' | 'manager';

    interface UserTypeCounts {
        [key: UserType]: number
    }
Not the best example, but it gets the point across. When you do this it says the key must be a string type which it actually is. It's just a string limited to specific values.

Yes, I could do an interface with explicitly named keys instead, but if that type (or enum) could have dozens of possible values it's annoying to duplicate it.

Isn't Record<EnumType, whatever> working?

Then instantiate like:

``` { [EnumType.First]: ..., [EnumType.Second]: ... } ```

Yep, do this all the time. It's also really nice because when you define a type as

    Record<EnumType, any>
then when you are creating an instance of that Record object, it requires a key for all the EnumType values, and will fail if you forgot one. Still possible to do

    Partial<Record<EnumType, any>>
if you want the keys to be only of the EnumType, but don't require all the EnumType values to be used as a key in the Record.
> - Tagged template strings. This just feels dirty to me. Probably won't use, but when I see it in a code base I won't be so confused at least

What's funny is the go-to example of using it for translations is just wrong: It only does numeric indexing, so can't be reliably used with languages where words would be in a different order. You still need a library or something that builds on top of it to handle that.

Realistically changing word order isn't enough for translation either, you need a special language like ICU message syntax so you can handle grammatical number, gender, etc.
Regarding your last point, I think the general consensus in TypeScript is to avoid using enums entirely.
What about union string types?

I'd really like to be able to just do...

    type UserType = 'default' | 'admin' | 'manager';

    interface UserTypeCounts {
        [key: UserType]: number
    }
"when I see it in a code base I won't be so confused at least"

When I first started using node way back when I discovered all kinds of idioms in use that I had never seen. It was a confusing few weeks for sure.

"??=, &&=, ||=" Yes they are, i stuck always when i see them. I think we just need more practice and need to see them more often until it becomes normal. Currently i avoid to use them.
> - Promise.allSettled. THIS is useful. I've implemented this (under a different name) in almost every code base I've worked on, and got bit HARD by not understanding this behavior long ago (huge production outage that took hours and many engineers to discover)

You and your team didn't understand how Promise.allSettled behaved?

No, Promise.allSettled didn't exist yet. It was Promise.all.

Someone basically made the assumption that await Promise.all would wait until ALL promises finished. Which is true..... unless one of them throws. In which case it continues. This caused a race condition. It was an extremely complex code base with 100s of engineers and the error very rarely happened, and when it did happen the app would get stuck on a loading screen forever. Also, it turns out "rarely" happens to a lot of people when you have millions of users.

I'm guessing the more common error, which I've definitely hit years ago, is not knowing that Promise.all returns immediately if any of the included promises reject.
Correct. It was millions of lines of code in a huge code base at a giant enterprise company with 100s of engineers. Somewhere buried in there there was a Promise.all that someone assumed would finish ALL of the promises, and didn't account for the fact that it bails on the first error.
That's what I'm guessing, too.
> Tagged template strings. This just feels dirty to me. Probably won't use, but when I see it in a code base I won't be so confused at least

A while back I wrote that "Tagged Template Literals Are the Worst Addition to Javascript" https://dmitriid.com/blog/2019/03/tagged-template-literals/ and I still stand by it.

The fact that someone uses them in a somewhat nice fashion in an sql library doesn't change the fact