JavaScript and TypeScript features of the last 3 years
medium.com
medium.com
like what?
type ServerResponse = {success:true, response:JSON} | {success:false, error:string};
In C++ I would probably have to type success:boolean, and make response and error optionals, but the TS type is more expressive.if success==true, then response must exist. if success==false, then error must exist.
const sr: ServerResponse = {success:true, error:"moo"); // won't compile, doesn't match
TS understands this type really well, and type narrowing makes it easy to work with. // NOT LEGAL; error might not exist
console.log(sr.error);
// IS LEGAL; we test for success first, which narrows the type.
if(sr.success){
console.log(sr.response);
} else {
console.log(sr.error);
}No, thanks.
I should have been specific, but all of the errors I mention above are compile-time errors, not run-time.
I used "success:true" and "success:false" as part of the two types I was combining; it seems like "success:boolean" would server the same function, but it does not. The type I created has more information that that.
https://www.typescriptlang.org/docs/handbook/literal-types.h...
- it's ability to infer types in general
- parsing. given a route string like `/users/:userId` TypeScript can force you to always pass `{ userId: 1234 }` when actually building the route without having to declare the interface anywhere
- conditional types
The optionality is a special trait that most "real" static languages don't get to enjoy (for better or worse). By being optional, I can simply ignore types when I'm hacking/prototyping something and type safety/soundness is the last thing I care about. Rust, for example, can feel really tedious when it makes me "show my work" and I'm just trying to scribble something up quickly. If that makes sense? I just make `any` a linting error so I can use it during my experimentation/prototyping but I cannot ship it.
Sounds pretty extreme..? What kind of bottlenecks would you be talking about that would prompt me to re-write my entire application in another language?
And TypeScript has a much more expressive type system compared to Go, Java, and frankly most languages. Java in particular still has pretty bad type reification (where ArrayList<Integer> and ArrayList<Float> are essentially the same and cannot be used to overload methods, for example).
I use a lot of the utility TypeScript functions (i.e. Partial<>, Record<>) and it catches all sorts of mistakes that might be made by myself in the future or someone not familiar with the codebase. For example, if there has to be a A->B mapping somewhere, I would type B so it would raise a type error if something was added in A but not B. Most type systems are not flexible enough to do anything remotely like that and instead you have to write a bunch of tests where you end up 20 lines of very basic test code for 1 actual line of code.
This:
type Foo = {
bar: string
// …
}
Can just as easily be addressed as: const parseFoo = parse.object({
bar: parse.string,
// …
})
type Foo = Foo<typeof parseFoo>
(Where parse methods here are type guards for their respective parsed return types, and can similarly be used to serialize runtime values as needed.)For that little bit of extra ceremony, you get to isolate runtime stuff to the boundaries you need to care about, and let the type checker deal with everything else (typically most everything internal).
TypeScript types are not available at runtime. As the name implies, runtime type information requires runtime support. Since TypeScript compiles to JavaScript, the only type information available is that provided by JavaScript.
I can't think of anything I can do in .NET (which has runtime types) that I can't do in TS with the help of a library or two.
Libraries like io-ts and zod embed the type information into JavaScript, so the information is available at runtime but only for wrapped types. If TypeScript provided runtime type information, such libraries wouldn't be necessary.
const parseStr = (value: unknown): assert value is string => {
if (typeof value === 'string') return value
throw new Error(…)
}
const str = parseStr(anythingYouCanThrowAtIt)
I can 100% guarantee you str is going to have the same static and runtime type once you’ve checked it, or parsed it from whatever type you’d accept as a string. TypeScript won’t do the parsing for you, because JavaScript doesn’t have clear semantics for runtime casting that anyone wants or would accept. But type guards are exactly the solution to that and incredibly composable. Once you accept that reality and embrace it, getting the static type out of your runtime parser is a single added line of static type code. And all of this is almost exactly equivalent to what languages with runtime casts do, but you have complete visibility into it because you determine how casts behave. There are whole libraries which do this for you so don’t worry about rolling your own unless you have very particular needs. But TypeScript definitely has the facility to align static and runtime types however you see fit. You just need to tell the type system what types the runtime conveys, same as every other aspect of the type system.It’s not a workaround. Every language with runtime types will have some logic devoted to this kind of casting/narrowing. TypeScript rightly doesn’t build it into the compiler because most of the time you don’t want types to have runtime behavior. For internal logic which can be validated statically, runtime types would be an unnecessary overhead. So it’s up to developers to determine where it should be applied. Type guards are explicitly an affordance for that, designed specifically to convey types with runtime casting/narrowing.
If you use good, composable primitives like zod or io-ts or any of several other implementations, your code will typically be almost identical to deriving runtime from types rather than the inverse.
0: https://itnext.io/parse-dont-validate-incoming-data-in-types...
1: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
My only issue with TypeScript is its strange design with regard to interface and types. Those are pretty fundamental concepts and they're absolutely similar. I don't understand this design and I think that only one thing should have left, but may be I'm wrong about it. Also I think that TypeScript documentation would benefit from more examples with hard concepts, I didn't fully understand its advanced generics concepts. But that's not a big thing and probably more on me.
I feel the same way. IIRC, earlier versions of Typescript were much more clearly influenced by .NET and other Microsoft idioms, and there were a few features that didn't seem to make much sense coming from the Javascript side of things. I also had a buddy of mine that's a Java developer be very confused about how they were supposed to work.
> Also I think that TypeScript documentation would benefit from more examples with hard concepts, I didn't fully understand its advanced generics concepts. But that's not a big thing and probably more on me.
Absolutely agree on this, although things are getting better. For awhile it seemed like the only documentation for more advanced features was the main website's blog posts and release notes.
- documentation
- aids tooling (eg improved static references in editor, static analysis by linter, etc)
- catches a whole class of problems before runtime (eg a typecheck-time substitute for certain runtime checks and their corresponding tests)
I often like to add:
- promotes better design
This puts TypeScript’s type system in a unique position, where it’s simultaneously expressive and complex in ways many others are not (because it’s designed to express extraordinarily dynamic idioms used in JS) and equally mundane and simple if you use it from the start (because all that complexity is a good forcing function for deciding whether you all of that dynamism it can express is of value for the thing you’re actually building).
In a more general sense, it strongly encourages developers to think about their interfaces explicitly where they might otherwise “rapidly prototype” a monstrous one.
The vast majority of the time, TypeScript will encourage simpler APIs that don’t need most of TypeScript’s “advanced” type system features. And in terms of productivity, that’s a benefit you only get if you’re starting from the proverbial Dynamic Wild West. So it might not be absolutely more productive than other statically typed languages, but still relatively an enormous productivity boost in its context.
In general, if a "best practice" of a language is to write less code that tends to translate to great productivity
As far as I can tell, TypeScript doesn't add any new features or behavior to JS other than static typing.
If you ask me, of course TS isn't as good as the real deal, but it's darn close and way better than vanilla JS or compilers for other languages that target JS.
- type system is way more flexible. You can create, merge, pick keys and manipulate types in all kinds of ways. You have better type inference, type conditionals and lots more
-there's an escape hatch. Sometimes you just want to quickly try something, or you're under time pressure and need to either break the types or do a bigger rewrite, or you just don't care about a certain variable's type. Then there's the any/unknown/plain js/@ts-expect-error escape hatches to do just that.
Yes, there are a couple of weird edge cases with arrow functions in generics, and it gets a bit more complicated with narrowing, but aside from that this never a problem I've run into. IMO the verbose error messages for complex types are a much bigger problem, which could be solved with better UIs for error messages (syntax highlighting and code folding).
Also after coding 5 days a week for a full year with it I really can't relate to the idea that it is somehow unpredictable. I think, like most things, you eventually get a "feel" for it and don't even have to think about it.
That's what I love most about it. It feels like it takes up no extra mental real estate for me. It's excellent at never "getting in the way" (when used properly)
https://www.typescriptlang.org/play#code/MYewdgziA2CmB00QHMA...
Practice safe interactions - say no to unprotected injections.
Who to blame is less important when you have an abandoned/orphaned child processes to take care of :/
No kink shaming, tho.
And I think it is the responsibility of the type system to at least catch if I'm passing too many arguments to a function.
I understand why they chose to do this, but I reach for better type systems any time I have the choice.
It took a long time for js devs to accept the notion of a compiler with TS. While TS is an improvement, it’s only a localized one. No chance ever, any other language could have replaced it.
There are perhaps at least a hundred (when not hundreds of) compile-to-js languages, yet people still use it. Even in back-end!
> It took a long time for js devs to accept the notion of a compiler
People who write programs in JS cannot be so easily grouped together, I think. There's this eternal September of bootcamp devs writing... interesting... code as we all did when we first started, but I personally went first with C, Perl, then PHP, then JS, then .NET, then Python and now continue mostly in TS and Java. Among them TS is the one I like the most (and the ones starting with P are the ones I dislike the most - not that it matters, use whatever works for you).
There are people ready to defend with their life the most obtuse, inane piece of technology, just because they have invested a non-negligible amount of time getting used to its quirks, that they become completely blind to its faults, and start to believe those very quirks are actually positive and to be cherished.
This is alludes to my only major complaint about typescript. IMO, something is either type-safe or it ain't. I don't like how the JS bleeds through if you aren't careful.
I'm not a TS hater by any means, I use it regularly and am usually pleased. I just can't help but compare it to the [S,Oca]ML compilers I'm so fond of.
> Me: oh, cool, they fixed so many tiny things I had bumped up against
> Some others: oh no, why are things changing
I'm not getting it. Maybe I'm reading this wrong, but to me these seem pretty obvious small issues to smooth over.The first comment I read after yours is literally "I hate how much programming languages change"
That, and for various reasons, it’s easy to use "import" everywhere in browser-side code, but painful to use "import" in Node. That’s a major selling point for ESBuild in my mind—I can avoid dealing with Node as much.
For example why introduce a new method to support negative indexing. Supporting `array[-1]` instead of `array.at(-1)` would mean one less thing to remember.
Many of the changes make the language feel like a hodge podge made from parts of other languages. This lack of cohesion is IMO what makes upgrading the language always feel like moved cheese.
For example, if you have a `binarySearch` function that returns -1 if an element isn't found, a developer might do something. `const result = arr[index]; if (result !== undefined) { ... }`. This would then start returning the last element instead of undefined at that index.
Changing browsers to interpret `arr.get(-1)` as `arr[arr.length - 1]` doesn't affect any old code using `arr[-1]`.
It's not about supporting old browsers. It's about supporting old code.
Adding new syntax and functions to the language is not a breaking change. Old code will continue to work.
If you start using these new features in your application, and it no longer works on old browsers, then sure that's a breaking change. But that's a choice for you to make. The language is still backwards compatible.
There's a valid example of code that would be broken (`indexOf` returns `-1` as "not found"). Is it a good way of solving whatever the author was trying to do? Probably not, especially now that sets exist. Is it code you might conceivably find on hunreds of sites across the past decades of the world wide web? You bet.
Yes, we could introduce another "use strict". But we only just got rid of the one via ESM (which enforces strict mode). That was a one-off hacky solution to a hard problem coming off the end of a failed major version release of the language (look up ECMAScript 4 if you get a chance). We don't want to see a repeat of that.
Surely there should be a simple way to have a header in each file with the language version, and then the file will be interpreted as that version?
You may find [2] and [3] especially enlightening to understanding this thinking, and any other discussions from ES Discuss on the topic if you fell like digging into history.
[1] https://johnresig.com/blog/ecmascript-harmony/
You grab some code in one of your old projects for implementing a binary search. Can you copy-paste it into a new project that targets a newer language version?
The question isn't as simple as "does it have syntax errors", because we're talking about changing semantics here. Given a set of semantic changes and a piece of code, figuring out (either as a human or a computer) whether the observable characteristics of that code have changed is somewhere between vexing and impossible. It's entirely possible, for example, that your code encounters changed semantics, but not in a way that changes the actual behavior of the code.
In this world it just becomes very, very difficult to reason about extremely common operations; it'd be a constant source of frustration. There's a good reason you rarely see languages versioning their behavior in impactful ways.
This is nothing JS specific. Breaking changes are breaking changes. If you can, don't introduce them.
> simple way to have a header in each file with the language version
One special aspect that differentiates JS from other languages:
It's both a language AND a universal runtime. A lot of JS that's executed is not JS that's written by humans but generated by a compiler/transpiler.
So adding a layer of header versioning is not a big win in terms of developer experience: It would anyways be the deployment toolchain that's responsible to deal with such a versioning scheme. It would ideally be invisible to the developer.
But what do I care, whatever mess and complexity arises from these "good enough" implementations is left for the generation after us to deal with :)
A lot of old code in other languages may be hard or impossible to compile and run without significant work.
You can add a polyfill to check if `Array.at()` exists, and if it doesn't, create a function that does the same thing and add it to the `Array` object, so now all `Array.at()` code works as expected.
Then once every environment you target supports `Array.at()` by default, you can remove the polyfill to reduce the size of your code.
if (array.at) {
explode();
}
Adding the at function would break the above code which depends on array.at being undefined.Before that point, browser environments were so different that you needed to write code per-browser. Those theoretical concerns didn't really matter since in-practice you were essentially coding the same app in different scripting languages.
Totally. It's just extending the prototype that causes the problem though, not extending from (class myclass extends array). This causes a lot of confusion among new js devs so I underline this on every opportunity.
This is also why the language got Array.prorotype.flat instead of flatten (flatten was breaking an old version of a popular library called Mootools): https://developer.chrome.com/blog/smooshgate/
IMO it was entirely good enough for what it does quite a while ago. No need to add more—that's purely introducing risk (of the language's ecosystem getting worse, mainly) from my perspective.
The only consistently annoying thing about it is how dumb it plays with JavaScript's built in array methods but it still is sufficiently smart 80% of the time (and the other 15% a simple `as const` does the trick)
It will live as long ad JS is popular. The main threat is web assembly which will make JS compatibility seem quaint.
In practice there seems to two different sides of typescript code.
1. Regular projects: tend to have rather easy to understand/write/maintain code.
2. Dependency code: tends to have code-golfed metaprogramming that the IDE uses to auto-magically autocomplete and highlight issues in regular project code.
- 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!!
enum TestEnum {
Fizz = 0,
Buzz,
Bar,
Baz
}
type EnumKeyedObject = Record<TestEnum, string>;
type EnumKeyedObjectAlt = { [P in TestEnum]: string }; 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.
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.
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'd really like to be able to just do...
type UserType = 'default' | 'admin' | 'manager';
interface UserTypeCounts {
[key: UserType]: number
}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.
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 }`; 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}`The tagged template does not return a string in this case?
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.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.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).
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.
Then instantiate like:
``` { [EnumType.First]: ..., [EnumType.Second]: ... } ```
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.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
You and your team didn't understand how Promise.allSettled behaved?
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.
Negative indexes might actually be useful.
At some point I actually need to read the actual language specs, I guess.
https://www.ecma-international.org/publications-and-standard...
But more seriously, languages that are less formally made and have grown organically all deal with these types of things. PHP is a great example of a language with a TERRIBLE core library filled with numerous "don't use this" and "yes this doesn't make sense" and esoteric foot guns.
Of course, these languages are the ones that took off and people use everywhere. And languages like PHP have made great strides to the point that using PHP8 with a modern set of libraries is not so bad.
Looking at other videos of Károly Zsolnai-Fehér speaking at a podium to an audience, I think he's genuinely enthusiastic about the subject instead of putting on an act. However, when combining that unusual enthusiasm with a Hungarian accent, some listeners may find it odd sounding.
Example vid: https://www.youtube.com/watch?v=-JdmOBA0WQ0&t=1m49s
...ironically in video format.
I actually hate videos, it's easier for me to read than to listen to someone taking 5 minutes to explain a paragraph's worth of stuff.
"It's longer because monetization."
A YouTube vblog calling itself two minute papers seems pretty ironic to me. The word "papers" is key here.
I feel like I am repeating myself.
Definition of irony...
Since we consume either articles (which are often rated as "This will take you 3 minutes to read") or video, the title is deceiving. Particularly as someone who finds consuming information infuriatingly slow in videos, and prefer to read or scan articles, I find it has a sense of irony to it.
Am I the only one? Am I a shit swe?
Edit - comments seem to suggest I was asking this question seriously. I was not, I was just (sort of) joking.
That being said, front end engineering is rapidly changing all the time, so the confidence I have in knowing I will always have work to do (read that to mean: a job) is satisfying.
If you find yourself struggling to articulate it, seriously and honestly consider whether you’re simply choosing to be stressed about it.
I’m speaking from experience here.
I mean they are adding features like static initialisation blocks when we only just got String.replaceAll(), and they somehow managed to fuck that API up despite it being explicitly a replacement for an existing bad API!
Where are all the containers? Sorted sets/maps? Why can't I even map an iterator?
That's W3C’s job, not ECMA’s.
> Where are all the containers?
?
> Sorted sets/maps?
Sets and Maps are sorted (by insertion order)
> Why can't I even map an iterator?
It's coming, but someone will likely be exhausted by that addition. https://github.com/tc39/proposal-iterator-helpers
That's not what a "sorted set" is. C++ and Rust provide sets (and maps) that sort based on an arbitrary comparison operator.
> ?
?
> but someone will likely be exhausted by that addition
I suppose. I guess it's one of those things that they really should have got right the first time. Like String.replaceAll(). I guess they will never add String.replaceAllSafe() - well just have to rely on linters to tell us that the API is terrible forever.
[1] https://docs.oracle.com/javase/7/docs/api/java/util/SortedSe...
My biggest learning while writing this was just how much is possible in JavaScript and TypeScript now, but I also realize that a lot of this I will not use myself or only use to understand some really specific code.
Then when I see it in the code, I'll know what it does or I'll be like "Oh wait, that new feature I read about, maybe that could be of use here".
I'm never going to remember everything and I'm okay with that :)
Why not? I was told the opposite: now that the feature is in JS natively, it can be used.
What I could conclude is: use `#` over `private` for runtimes that support them or projects that can target recent runtimes and browsers.
[0] https://deno.land/manual@v1.29.3/references/contributing/sty...
[1] https://google.github.io/styleguide/tsguide.html#private-fie...
I'd also argue that the majority of these changes are to the standard library (new methods on String, Array, RegExp, etc). Not really core language changes. I don't know how often the go libs update but surely faster than the language spec?
What?
In fact this is how TypeScript is implemented.
I know the quote you're referring to but you're stretching a mile to get there.
in JS (and years before that, PHP), one example was object-oriented programming / classes, except in both JS and PHP it was never implemented fully, with JS not even having access modifiers for a long time. JS didn't need classes and the implementation is lacking, but someone decided it should be added.
Likewise, Java had functional programming bolted on; they never extended the base List types with functional modifiers, so now you have to transform or wrap your List in a Stream to make it work with Java's attempts at functional programming. Personally, I think if you want to do FP or FP-style coding on the JVM, you should rewrite things in Scala. You can write Scala and Java side-by-side.
Same with JS, you want JS but with types? You can have Typescript. I wish they did the same with JS-with-classes, just write a new language that compiles to JS instead of bolt classes onto the JS standard.
But I greatly dislike multi-paradigm programming languages. Mostly because I've worked with other programmers.
Borrowing features (idioms) from other languages is great. Pattern matching is nice. For Java, I'm looking forward to (interpolated) string templates and implicit classes. Destructuring would be nice too.
--
But for the love of Larry, where are intrinsic regex expressions?
Path expressions?
Most of our work is data processing.
Input -> munging -> output.
Meaning cutting and pasting strings.
So I mostly want new features related to string and data processing.
--
The evergreen fetish (kink) with arcana like type systems, monads, and metaprogramming is just so besides the point. That's what Lambda the Ultimate and HN are for.
For "commercial" languages, just give me tools for work.
Fussing with novel languages is for my hobby projects.
(I agree with your overall point BTW.)
I just checked; the MDN docs refer to them as "regular expression literals".
Jeremy tried https://arcturo.github.io/library/coffeescript/03_classes.ht...
I think the classes were a good thing. Better would have been to change the OO model entirely, but if you're going to have one and can't change it, nice to at least have some sugar to make it usable. IDK if it's better elsewhere, but prototypal OO was pretty clearly a miss-step for Javascript, and sugar to make the OO system usable and less-obtuse (while making it easier to ignore the parts that should basically never be used, which parts amount to all the distinctive things about prototypal OO) is a decent move.
Wow, relevant username for this take, as you'd end up with an eldritch creature as soon as people tried to bolt these DSLs together into their opinionated bags of features, and a meta-language landscape that resembles the already fragmented frontend landscape of today
In the case of TypeScript, we have the base JavaScript, template literals, and the type system all playing this game in the same file. Three systems trying to out feature each other. Tagged template literals, template literal types, etc.
I’m not entirely sure this is a good thing. But it’s certainly convenient in some instances.
I used Go wrong for years. Once I stopped doing dumb stuff with it (especially with the type system), it got a lot more pleasant. I don’t think I’d design it the same way, but I’m quite a bit happier using it now.
It’s kind of like using a hammer to drive a screw. The problem isn’t the screw or the hammer. Go seems to be a hammer-screw situation for a lot of people, but there really is a happy path.
Not saying this because I think you’re unaware — mostly thinking out loud because I used to feel like Go needed new features and your comment reminded me of this. I also like the relative stability of Go now, though. It has a great foundational toolset and good design overall, so to its credit, it hasn’t really needed to change so much. I only thought it did because I used it wrong.
Supposedly it's because of this:
> Why isn't access this.x?
> Having a private field named x must not prevent there from being a public field named x, so accessing a private field can't just be a normal lookup.
https://github.com/tc39/proposal-class-fields/blob/main/PRIV...
Combined with this:
> Why aren't declarations private x?
> This sort of declaration is what other languages use (notably Java), and implies that access would be done with this.x. Assuming that isn't the case (see above), in JavaScript this would silently create or access a public field, rather than throwing an error. This is a major potential source of bugs or invisibly making public fields which were intended to be private.
https://github.com/tc39/proposal-class-fields/blob/main/PRIV...
But I still think it's weird.
Same goes for methods that you have to manually bind to `this` in the constructor etc.
It literally is one of the "they didn't think if they should" parts of the language.
This is wrong. Array.at can get, but not set the value. So they're not equivalent.
console.log( arr[arr.length-1] ); // works
arr[arr.length-1] = 1; // works
console.log( arr.at(-1) ); // works
arr.at(-1) = 1; // ReferenceError: Invalid left-hand side in assignment
Just the way it extracts the substrings into arguments for unpredictable strings. It doesn't translate to readable code.
I'd much rather use repetitive template strings. Unless I was doing some really fancy string manipulation. Or make my own functions with explicit arguments.
const thingies = ["a", "b"];
const config = yaml`
- foo
- bar
- baz
- thingies: ${thingies.map(thing => yaml`- thing: ${thing}`)}
`;
// config = ["foo", "bar", "baz", { "thingies": [{"thing": "a"}, {"thing": "b"}] }]
At a company I worked at people were generating YAML using Jinja templates, but tagged templates to me would be a better approach. Is it hard to read? It can be, but compared to the alternatives it's not too bad. const thingies = ["a", "b"];
const thingToYaml = thing =>
yaml`- thing: ${thing}`
const thingiesYaml =
thingies.map(thingToYaml)
const config = yaml`
- foo
- bar
- baz
- thingies: ${thingiesYaml}
`;
It doesn’t have to be hard to read const thingies = ["a", "b"];
const config = [
"foo",
"bar",
"baz",
{
thingies: thingies.map(thing => ({ thing })),
},
]
// config = ["foo", "bar", "baz", { "thingies": [{"thing": "a"}, {"thing": "b"}] }] var result = await query`
SELECT * FROM foo.bar where baz = ${baz}
`;
There are libraries that allow for construction of queries as well using template strings. I don't know of too many instances beyond this where there's custom functions for tagged templates, vs just string building with parameters.It's nifty, but not nearly better enough to justify its existence, IMO. Here's the alternative:
var result = await query(
'SELECT * FROM foo.bar where bar = :baz',
{baz}
);
I get it... there's that extra level of indirection. But people are working hard, as we speak, to abuse the feature.It's not like "figure out how to parse the string, extract the template slots, and pass the correct arguments into the correct slots" is rocket science.
And it's not like people are going to rewrite the numerous existing libraries for this kind of thing. The new tagged-template APIs are going transform their arguments and call the existing APIs.
I guess it's nice that new Javascript-specific templating languages can have common escaping syntax. It's just hard to get excited about the 15th standard.
var result = await query(
'SELECT * FROM foo.bar where bar = :baz
-- 100 more lines of where clauses, CTEs, etc.
',
{baz} // where did I use this again? I'd need to scroll up.
);
versus var result = await query(
sql'SELECT * FROM foo.bar where bar = ${baz}
-- 100 more lines of where clauses, CTEs, etc.
`); dedent`something ${code}`
and dedent(`something ${code}`)
? Not sure to understand the advantage of tagged strings here... function exampleTag(stringSegment, ...values) {
console.log(stringSegment);
console.log(values);
}
const myNumber = 123;
exampleTag`foo ${myNumber} bar ${myNumber}`;
// Prints:
// ['foo', 'bar', '']
// [123, 123]
This gives the tag function a way to access the interpolated values themselves, before they get coerced to a String. You're correct, though, that it really doesn't make much of a difference for a dedent function. function fmt(q, a) {
return dedent`\
Question: ${q}
Answer: ${a}
`;
}
Since `dedent` is basically syntactic sugar for developer convenience, we want this function to be equivalent to this: function fmtDesugared(q, a) {
return `Question: ${q}\nAnswer: ${a}\n`;
}
Now consider this input: console.log(fmt("a\nb", "c\nd"));
// should print:
Question: a
b
Answer: c
d
But if `dedent` only got to see the string after interpolation—like this— function fmtWrong(q, a) {
return dedent(`\
Question: ${q}
Answer: ${a}
`);
}
then the input to `dedent` would be Question: a
b
Answer: c
d
and so `dedent` would not strip any indentation. That is, `dedent` is meant to identify the maximal common leading indentation in the template as written by the developer, which should not depend on the values of the interpolands.A stale GitHub issue on the dmnd/dedent repo has a real-world example where this matters and led to a subtle bug: https://github.com/dmnd/dedent/issues/22
We should be able to write multi-line strings without having to break our linter's tab rules or write some silly post processing function!
I've been running this project straight in the browser with no JS compiler. Are JS compilers going to be on the way out now?
Ohh, sorry, you mean JavaScript... const is the new var because Java programmers don't understand function scope. ()=>{} is the new function declaration because many people want to go back to Perl like syntax. class instead of prototype because Java programmers don't understand prototype. TypeScript instead of JavaScript because Java programmers can't live without type annotations and editor autocomplete. async/await instead of Promise because what we can't see (async code) can't hurt us. import instead of require because JetBean intellicense could not autocomplete dynamic require/imports.
At this point I'm almost afraid to ask, but if JS can evolve in major ways like this, why can't we address some of the basic shortcomings of the language?
edit: Edge and Safari only added support in 2020, so it has only been useable without polyfills for a few years
Breaking existing code is a pretty major line to cross for anyone maintaining a language, library or tool. (Even though some people do it willy nilly....)
14-year old me hacking up my first JS on Freewebs.com is doing backflips right now.
...And 31-year old me is saying "What the fuck took them so long?"
Also your code for "Tuple Optional Elements and Rest" is broken. Pad is declared twice and it's returning the wrong thing.
In general, `import * from X` will pull every exported symbol of X into your current module at the top level (generally not recommended except for special cases like testing libraries; you shackle yourself to an assumption that the imported module won't ever add new symbols that might interact surprisingly with the importing module). `import * as Y from X` is slightly safer; it'll pull in all the exported symbols from X, but will wrap them in a Y namespace (so X's `foo` function is now `Y.foo`, etc.). `import {foo} from X` will just import the `foo` symbol from X and make it available in your module. Finally, `import foo from X` imports the single export in X that is tagged as default and names it `foo` in your module.
(To my money, I don't like defaults very much. I much prefer `import {foo} from X` over `import foo from X` for clarity, even if `foo` is the only symbol X exports. It allows for future growth of X and avoids the unsightly `import foo, {bar} from X` that some modules end up growing in the future).
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
someModule.js:
export default 'foo';
export const bar = 'bar';
namespaceImport.js: import * as SomeNamespace from './someModule.js';
console.assert(SomeNamespace.default === 'foo');
console.assert(SomeNamespace.bar === 'bar');
defaultImport.js: import foo from './someModule.js';
console.assert(foo === 'foo');
namedImports.js: import { default as foo, bar } from './someModule.js';
console.assert(foo === 'foo');
console.assert(bar === 'bar'); - export default foo = 'foo';
+ export default 'foo';