Live data from Hacker News

Node.js adds experimental support for TypeScript

github.com

421–430 of 570 posts

Re: Node.js adds experimental support for TypeScript

#421
post #413

Earlier quoted context omitted.

Because I don't like breaking things unnecessarily. Some of my libraries are 10 years old and depended upon by similarly old projects that are not using ESM and probably never will. Besides, it's already going through one transpilation step to go from TS to ESM, so adding a second one for CJS really isn't that much hassle. I think if node.js had made require() work with ESM, I could probably drop CJS. But since that'…

> adding a second one for CJS Nobody is arguing for that. Once you ship ESM, you can continue shipping ESM. In Node 22 you can even require() ES modules (with an experimental flag, at the moment)

> > adding a second one for CJS

> Nobody is arguing for that. Once you ship ESM, you can continue shipping ESM.

I'm not sure I follow you there. I did continue shipping ESM.

> In Node 22 you can even require() ES modules (with an experimental flag, at the moment)

Oh, I didn't know about that, cool! Once it becomes un-flagged I might consider dropping CJS.

Re: Node.js adds experimental support for TypeScript

#422
post #357

Earlier quoted context omitted.

Flow (by Facebook) used to be fairly significant in the JavaScript several years ago, but right now it's somewhat clear that TypeScript has won rather handily.

There was no real competition, Flow was a practical internal tool with 0 marketing budget. Typescript is typical MS 3E strategy with a huge budget. Needless to say, Flow is much more practical and less intrusive, but marketing budget captured all the newbie devs.

TypeScript was really really easy to get started with back in the day. It allows for incremental correctness, has good docs, and good tooling. On top of that a lot of beginner React tutorials started out with TypeScript, which onboarded a lot of new engineers to the TS ecosystem, and got them used to the niceties of TS (e.g. import syntax).

Re: Node.js adds experimental support for TypeScript

#424

Earlier quoted context omitted.

Why are you still transpiling to .cjs in 2024? ESM is supported in every LTS version of Node now. We can kill CJS, we have the power.

Because I don't like breaking things unnecessarily. Some of my libraries are 10 years old and depended upon by similarly old projects that are not using ESM and probably never will. Besides, it's already going through one transpilation step to go from TS to ESM, so adding a second one for CJS really isn't that much hassle. I think if node.js had made require() work with ESM, I could probably drop CJS. But since that'…

> I think if node.js had made require() work with ESM, I could probably drop CJS

Why is making downstream have to switch to `await import()` that big of a deal?

You can use async/await in CJS just fine. Sure, sometimes you may need to resort to some ugly async IIFE wrappers because CJS doesn't support top-level await like ESM does, but is that really such a big deal?

Sure, it's a breaking change, but that's what semver major bumps are for.

I just think that if projects want to stay in CJS they should learn how to use async/await. I clearly don't understand why CJS libraries feel a need synchronous require() for everything. (Though to be fair, I've also never intentionally written anything directly in CJS. I learned enough in the AMD days to avoid CJS like a plague.)

Re: Node.js adds experimental support for TypeScript

#425

Earlier quoted context omitted.

They did just add a new keyword, satisfies, in 5.4. That would be a breaking change if you can’t upgrade the type stripper separately.

This is true, but in other cases they added keywords in ways that could work with type stripping. For example, the `as` keyword for casts has existed for a long time, and type stripping could strip everything after the `as` keyword with a minimal grammar. When TypeScript added const declarations, they added it as `as const` so a type stripping could have still worked depending on how loosely it is implemented. I thin…

I don't know a lot about parser theory, and would love to learn more about ways to make parsing resilient in cases like this one. Simple cases like "ignore rest of line" make sense to me, but I'm unsure about "adversarial" examples (in the sense that they are meant to beat simple heuristics). Would you mind explaining how e.g. your `as` stripping could work for one specific adversarial example?

    function foo() {
        return bar(
            null as unknown as T extends boolean
            ? true /* ): */
            : (T extends string
                ? "string"
                : false
            )
            )
    }

    function bar(value: any): void {}
Any solution I can come up with suffers from at least one of these issues:

- "ignore rest of line" will either fail or lead to incorrect results - "find matching parenthesis" would have to parse comments inside types (probably doable, but could break with future TS additions) - "try finding end of non-JS code" will inevitably trip up in some situations, and can get very expensive

I'd love a rough outline or links/pointers, if you can find the time!

[0] TS Playground link: https://www.typescriptlang.org/play/?#code/AQ4MwVwOwYwFwJYHs...

Re: Node.js adds experimental support for TypeScript

#426

Earlier quoted context omitted.

Because I don't like breaking things unnecessarily. Some of my libraries are 10 years old and depended upon by similarly old projects that are not using ESM and probably never will. Besides, it's already going through one transpilation step to go from TS to ESM, so adding a second one for CJS really isn't that much hassle. I think if node.js had made require() work with ESM, I could probably drop CJS. But since that'…

> I think if node.js had made require() work with ESM, I could probably drop CJS Why is making downstream have to switch to `await import()` that big of a deal? You can use async/await in CJS just fine. Sure, sometimes you may need to resort to some ugly async IIFE wrappers because CJS doesn't support top-level await like ESM does, but is that really such a big deal? Sure, it's a breaking change, but that's what semv…

> Why is making downstream have to switch to `await import()` that big of a deal?

> You can use async/await in CJS just fine. Sure, sometimes you may need to resort to some ugly async IIFE wrappers because CJS doesn't support top-level await like ESM does, but is that really such a big deal?

It might seem like a small amount of work, but for a library one must to multiply that small amount of work by the number of users who will have to repeat it. It can be a quite large amount in aggregate. And, for what benefit? So I can drop one line from my CI config? It just seems like a huge waste of everyone's time.

Also, as a library user, I would (and occasionally do) get annoyed by seemingly unnecessary work foisted on my by a library author. It makes me consider whether or not I want to actually depend on that library, and sometimes the answer is no.

Re: Node.js adds experimental support for TypeScript

#427
post #410
post #130

Earlier quoted context omitted.

Union types, structural typing, and conditional types are like a big chunck of what makes typescript typescript. It is how TS is able to "type" a completely untyped language. Just the support for union types is something that not even Haskell or Ocaml have.

I am not familiar with TypeScript. Is there something that you can achieve with union types that you can’t with sum types or type classes in Haskell?

TL;DR: Typescript is unsound so it can add a lot more type-level features that would make a sound type system undecidable

Conceptually no, almost every useful union type can be easily converted to a sum type. In my opinion the difference is in the ergonomics and in the implicit structural subtyping.

For example a common union type is number|string, and the beatiful part is that to use a value of such a type you do not need to do any matching or mapping you can just use the value as it does not have a runtime wrapper, for example (x:string|number)=>JSON.stringify(x) works perfectly fine.

Also you can have a function that takes as input a Array|number|null and returns a string|number without having to declare different contructors for the input number type and the output number type

I believe that you can essentially implement this behaviour by generating enough typeclasses in Haskell, but regardless of the feasibility it, likely, would not be a good idea.

An example of something in between union types an Hindley–Milner sum types are Ocaml's polymorphic variants types https://ocaml.org/manual/5.2/types.html#sss:typexpr-polyvar that are (I believe) more advanced than TS unions but also a lot less ergonomic to use.

And TS has much more eg intersection types you could have a function with type

    (x:number)=>string & (x:string)=>number
meaning that it is both a function that maps number to strings and strings to numbers (again you can do this with typeclasses but it is a worse experience)

typescript also has very good support for value types for example there is the string type but also the "hello" type which is the type of only the string "hello"

All in all if someone told me that they implemented typescript in haskell typeclasses I would not call bullshit on them, but I would not believe that anyone would actually use it for anything

Re: Node.js adds experimental support for TypeScript

#428
post #399
post #363

Earlier quoted context omitted.

If there's a bad way to write JS, TS has something available to make sure it's typed. Does TS help you keep your functions monomorphic so they'll get optimized by the JIT? nope Does TS keep your object shape from changing so it will get optimized by the JIT? it actively does the opposite giving TONS of tools that allow you to add, remove, modify, and otherwise mess up your objects and guarantee your code will never o…

> Does TS keep your object shape from changing so it will get optimized by the JIT? it actively does the opposite giving TONS of tools that allow you to add, remove, modify, and otherwise mess up your objects and guarantee your code will never optimize beyond the basic bytecode (making it one or two orders of magnitude more slow than it could otherwise be). Can you elaborate or point to some of the tools? So I know w…

JS JITs use something called an inline cache (IC) to speed up the lookup of object shapes. JS JITs consider it to be a different shape if the keys are different (even if just one is added or removed), if the values of the same key are different types, and if the order of the keys change.

If you have a monomorphic function (1 type), the IC is very fast. If you have a polymorphic function (2-4 types), the IC function gets quite a bit slower. They call 5+ types megamorphic and it basically foregoes IC altogether and also disables most optimizations.

TS knows how many variants exist for a specific function and even knows how many of those variants are used. It should warn you when your functions are megamorphic, but that would instantly kill 90% of their type features because those features are actively BAD in JS.

Let's illustrate this.

    interface Foo {
      bar: string | string[]
      baz?: number
      blah?: boolean
    }
Looks reasonably typical, but when we use it:

    function useFoo(foo: Foo) { .... }

    useFoo({bar: "abc", baz: 123, blah: true}) //monomorphic
    useFoo({bar: "abc", baz: 123})             //now a slower polymorphic
    useFoo({bar: "abc"}) 
    useFoo({bar: ["b"], baz: 123}) 
    useFoo({bar: ["b"], baz: 123, blah: true}) //we just fell off the performance cliff
As you can see, getting bad performance is shockingly easy and if these calls were across five different files, they look similar enough that you'd have a hard time realizing things were slow.

Union/intersection aren't directly evil. Unions of a single type (eg, a union of strings) is actually great as it offers more specificity while not increasing function complexity. Even if they are a union of different primitive types, that is sometimes necessary and the cost you are paying is visible (though most JS devs are oblivious to the cost).

Optionals are somewhat more evil because they somewhat hide the price you are paying.

[key:string] is potentially evil. If you are using it as a kind of `any`, then it is probably evil, but if you are using it to indicate a map of strings to a type, then it's perfectly fine.

keyof is great for narrowing the possible until you start passing those keys around the type system.

Template unions are also great for pumping out a giant string enum (though there is a definite people issue of making sure you're only allowing what you want to allow), but if they get passed around the type system for use, they are probably evil.

Interface merging is evil. It allows your interface to spread across multiple places making it hard to follow and even harder to decide if it will make your code slow.

Overloads are evil. They pretend you have two different functions, but then just union everything together.

Conditional types are evil. They only exist for creating even more complex types and those types are basically guaranteed to be both impossible to fully understand and allow very slow code.

Mapped types are evil. As with conditional types, they exist to make complex an incomprehensible types that allow slow code.

Generics are the mother of all that is evil in TS. When you use a generic, you are allowing basically anything to be inserted which means your type is instantly megamorphic. If a piece of code uses generics, you should simply assume it is as slow as possible.

As an aside, overloads were a missed opportunity. In theory, TS could speed everything up by dynamically generating all those different function variants at compile time. In practice, the widespread use of generic everything means your 5mb of code would instantly bloat into 5gb of code. Overloads would be a great syntax to specify that you care enough about the performance of that specific function that you want to make multiple versions and link to the right one at compile time. Libraries like React that make most of their user-facing functions megamorphic could probably see a decent performance boost from this in projects that used TS (they already try to do this manually by using the megamorphic function to dispatch to a bunch of monomorphic functions).

Post reply on HN