Hacker News new | past | comments | ask | show | jobs | submit login
TypeScript is surprisingly ok for compilers (matklad.github.io)
300 points by Fudgel on Aug 18, 2023 | hide | past | favorite | 231 comments



TypeScript is an incredible language in general.

The fact that Functions are Objects that can have properties/methods is supremely undervalued.

Are there other languages that do this so nicely? It's the perfect blend of OO and functional.

Programming is mostly about gradually figuring out the right design I find. JS/TS let's me evolve things naturally without big rewrites.

    function foo() {}
    function bar() {}
    function baz() {}
    const commands = [foo, bar, baz]
    // Run commands
    commands.forEach(x => x())

    foo.help = 'This does something'
    // Describe commands
    commands.forEach(x => console.log(x.help))

    // Add some state via closure.
    const config = {}
    function foo() { config.blah }

    // Add some state using partials.
    function _foo(config) { config.blah }
    const foo = foo.bind(null, config)
I can flexibly do what I need, without ever having to define a class like `Command`, which I probably don't even know what it should be yet.

This premature naming of things in OO creates so many dramas.

It avoids things like `VideoCompressor#compress()`, `VideoSplitter#split()`. You don't have to think: what state belongs in which class...you just call the function and pass it what it needs.


TypeScript is really nice but you're really describing a JavaScript feature to be honest.

Here's some other languages that support objects as functions (although not necessarily functions as objects): https://en.wikipedia.org/wiki/Function_object


I suspect many ts programmer dont even know it compiles into js. Potentially dont even know what js is. Such is the state of modern web dev.


In Go, you can attach methods to functions (and any type really, even "unboxed" primitives).

One of the canonical examples would be a net/http: A handler can both be a struct (or any other type really) that implements a serve method, or it can be a handler function that calls itself to satisfy the interface.

In Clojure you would achieve the thing you describe by attaching metadata on your function var. It being a Lisp, it also has macros so you can pretty much do anything.

Speaking of macros:

Clojure also implements CSP channels with macros in core/async, inspired by Go.

Channels are a very powerful construct that you might like a lot. With channels you can completely avoid the function coloring problem (callbacks, promises, async/await). Perhaps most importantly they decouple execution from communication.

So going back to your example, your commands could be producers that send their results on channels. They don't need to know what consumes w/e they make, nor do they need to hold on to it, mutate something, or call something directly.

Good analogies would be buses on motherboards, routers and switches in networks, conveyor belts, message queues in distributed systems and so on.


The same is achieved with Java's use of single-method-interfaces. It doesn't matter what the method is called, it can be used in function contexts without referencing the specific method name.

I'm not sure I'd like my functions to have properties (which gives them state and can alter what calling it does with the same arguments). A big benefit of FP is getting away from OO states. Perhaps the problems I work on aren't complex enough to benefit from them, or I simply make objects from classes.


To be clear, since "can be used in function contexts without referencing the specific method name" might be ambiguous to some- in Java you still call the "function" as if it was a class implementing the interface. IE: `interface Foo { String bar(); }` is still called as foo.bar().

It's just that there's now syntactic sugar for creating anonymous classes: `Foo foo = () -> "baz"` that automatically expands to an anonymous class that conforms to `Foo`. The compiler automatically assigns the method name for you.


Interesting, didn't know about single-method-interfaces.

There are lots of singletons in OO. I find it useful to add static metadata (not state) to them, without having to escalate them to a class. I guess Java has decorators for the same purpose. I'd really like decorators for functions in TypeScript though.


The properties don't have to be 'state' per se; they can also be used to store metadata about the function. e.g. you could have a function with 'domain' and 'range' on it, or an 'integrate' method.


If the result of `integrate(...)` depend on `domain` and/or `range`, what would you call that other than 'state'? Or if `integrate(...)` doesn't reference `domain`/`range` what's the use of it being there?

Or as I'm interpreting this, is sort of like documentation or information that could be used at runtime for code generation. Basically a shorthand for composing the function with its metadata. The nice thing about separation is that you know that when calling the function, there's no possible way for it to reference the metadata that is associated with it because it's composed externally.


Avoiding this type of state is essentially impossible in FP, as captured bindings of closures have the same properties


>>> Are there other languages that do this so nicely? It's the perfect blend of OO and functional.

Python, where everything is an object.


FP in python is painful without tail call elimination and the higher-order function syntax is so clunky


JS also doesn't have TCE, but for Python even just the lambda limitations are surprisingly annoying. I can't tell you how many times i've been frustrated because it's nearly impossible to put a print statement into a python lambda


As a Python enjoyer, why do we want to shove so much into lambdas rather than just doing an inline function `def`?

Is it the fact that you have to give it a name? If so I'd say just using some generic name like `{f,fn,func(tion),callback,etc}` is fine (at least as much as an anonymous function is), and to me would usually be more readable than an inline definition of a multi-statement lambda would be.

Or maybe it's the fact that lambdas are allowed in the first place, so people are going to use them, and then when you want to debug a lambda you'll probably have to go to the trouble of changing it to a function? That is a fair complaint if so.

In any case I can see how it could be annoying if you're more used to a language full of complex inline lambdas.


JS also kills Python for inline functions thanks to hoisting.

It's much easier to follow the control flow with hoisting. I see `run()` being called, and then I want to know what it is. In other languages you are usually seeing a huge bunch of inline functions and then asking: okay, but when and how is this actually called?

    def foo():
      def run():
        print("hi")
      run()

    function foo() {
      run()
      function run() {
        console.log('hi')
      }
    }


honestly I don't like this style of writing... in your js example I see `run()` and my first though is where the hell is run defined? is a global? I don't search - nor I write - the called function AFTER the calling ones, it seems backward to me.

moreover... basically any half-decent programmer text editor has an outline with the list of functions, so this point may be moot in any way


Lambda is quite clunky. A lot is possible by abusing tuples and walrus assignment, which Ive on occasion used for one liners. e.g. you want to execute a function for each element of a list (print is a function in py3) so mapping over a generator with eg

    map(lambda x: (x := func1(x), func2(x), None)[2], gen())
This sets x to func1(x), then executes func2, then leaves None in place of the element in the map iterable. (of course you could do the same with a list comprehension, you wouldn't even need the lambda in that case, and good python would _actually_ be a for loop.)


That's a mismatch between Python's choice of lambda syntax and the use of whitespace instead of curly braces.


lambda x: print(x) or x


JS technically does have TCE (specified in ES6[1]), but only the JavaScriptCore runtime used by Safari/WebKit implements it[2].

[1]: https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-... [2]: https://kangax.github.io/compat-table/es6/#test-proper_tail_...


In Python, can typings define a property added to a function?

Example, I have some Redux helpers that are functions, but those functions also define a `.actionType` property. TypeScript handles that.

edit: accidentally wrote "object" instead of "function"


The typical approach to this in Python is to define a callable class instead. If you define the __call__() method on a class, instances of the class will be callable.

  class IntAdder:

      def __init__(self, x: int) -> None:
          self.x = x

      def __call__(self, y: int) -> int:
          return self.x + y

      def __str__(self) -> str:
          return f"({self.x} + ??)"

  add1 = IntAdder(1)
  assert add1(3) == 4
  print(add1)
  # (1 + ??)
As mentioned in the sibling comment, you can define a Protocol which is analogous to an interface, so if you had a protocol like this:

  from typing import Protocol, TypeVar

  T = TypeVar("T")

  class ValueUpdater(Protocol[T]):
      def __call__(self, y: T) -> T: ...
Then IntAdder would be considered a subclass of ValueUpdater[int] by the type checker. Demo: https://mypy-play.net/?mypy=1.5.1&python=3.11&flags=strict&g...


I believe you could have a Protocol that requires the property and that the object is Callable.


Ah, that's not really that different than in TypeScript then. There you have to define a callable interface, and can add whatever properties you want

    interface SomethingCallableWithAnExtraProperty {
      // This means you can call it like a function.
      (args: Whatever): SomethingReturned

      // And since it's an interface, you can still do interface-y things
      anotherProperty: string
    }


    The fact that Functions are Objects that can have properties/methods is supremely undervalued.
    
    Are there other languages that do this so nicely? It's the perfect blend of OO and functional.
Yes. C#. The equivalent are `Func` and `Action` types representing functions with a return and without a return. In fact, the JavaScript lambda expression looks awfully familiar to C#.

One of the snippets below is C# and the other is TypeScript:

    var echo = (string message) => Console.Write($"You said: {message}");
    
    var echo = (message: string) => console.log(`You said: ${message}`);
The same signature in C# and TypeScript:

    var Apply = (Func<string, string> fn, string input) => fn(input);
    var result = Apply(input => ..., "some_string");
    
    var apply = (fn: (input: string) => string, input: string) => fn(input)
    var result = apply(input => ..., input);
The C# can version can also be written like:

    var Apply = (Func<string, string> fn, string input) => fn(input);
    var lowercase = (string input) => input.ToLowerInvariant();
    var result = Apply(lowercase, "HELLO, WORLD");
For the curious, I have a small repo that shows just how similar JavaScript, TypeScript, and C# are: https://github.com/CharlieDigital/js-ts-csharp

Screen grab of the same logic in JS/TS/C# showing the congruency: https://github.com/CharlieDigital/js-ts-csharp/blob/main/js-...


Great repo. I've always wanted to build a transpiler from TS to every other language. We have so many languages but all syntax is so similar in the end.

I often wonder how much of the code we write is actually doing something not possible in another language. Like runtime-specific, or low-level stuff. Most of the business logic...loops and if statements are rather similar.


this doesn't really address OP's point, where in JS you can do:

    const foo = () => doSomething;
    foo.help = "this is a description of the function";
    const commands = [foo];
    
    // print help
    commands.forEach(c => console.log(c.name, c.help || "No help is available for this function");
Presumably this isn't possible in C# because it's statically typed, so the object returned by "() => doSomething" can't be converted into one that supports adding more properties?


It can.

.NET/C# has a `dynamic` type (aka `ExpandoObject`). That would be one way to do it (but would require casting to invoke). It's not exactly the same since you'd assign the `Func`/`Action` to a property of the `dynamic`. `dynamic` is generally avoided due to how easy it is to get into a pickle with it and also performance issues.

An alternate in this case is probably to return a tuple which I think is just as good/better.

Example:

    var log = (object message) => Console.WriteLine(message);
    var foo = () => log("Hello, World");
    var fn1 = (foo, "This is the help text");

    var commands = new[] { fn1 };
    commands.ToList().ForEach(c => {
      var (fn, help) = c;
      log(fn.Method.Name);
      log(help ?? "No help is available for this function");
    });
https://dotnetfiddle.net/szxb9F

The tuple can also take named properties like this:

    var log = (object message) => Console.WriteLine(message);
    var foo = () => log("Hello, World"); 
    
    var commands = new (Action fn, string? help)[] { 
      (foo, "This is the help text"), 
      (foo, null) 
    };
    
    commands.ToList().ForEach(c => {
      log(c.fn.Method.Name);
      log(c.help ?? "No help is available for this function");
    });
https://dotnetfiddle.net/oPK1d8

Alternatively, use anonymous types:

    var log = (object message) => Console.WriteLine(message);
    var foo1 = () => log("Hello, World");
    var foo2 = () => log("Hello, Neighbor");
    
    var bar = new[] {
      new {
        doSomething = foo1,
        help = "This is foo1's help text"
      },
      new {
        doSomething = foo2,
        help = "This is foo2's help text"
      },   
    };
    
    bar.ToList().ForEach(b => {
      var (fn, help) = (b.doSomething, b.help);
      fn();
      log(help);
    });
https://dotnetfiddle.net/KyBtgZ

The next release of C# (12) will include named tuple types: https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

Very much looking forward to this since it gives you a lot of the same power of the JavaScript map/TS `Record`.

    > ...because it's statically typed
While this is true, the `dynamic`/`ExpandoObject` is an oddity and lets you do weird things like multiple dispatch on .NET (https://charliedigital.com/2009/05/28/visitor-pattern-in-c-4...). But C# has a bunch of compiler tricks with regards to anonymous types that can mimic JS objects to some extent. The tuple type is probably a better choice in most cases, however.


> Are there other languages that do this so nicely? It's the perfect blend of OO and functional.

Python

  compressor = lambda x: x / 2
  compressor(5)

  helpful_compressor = lambda x: x / 2
  helpful_compressor.help = "compressor"
  helpful_compressor(5)
  helpful_compressor.help
Scala

  val compressor = (x: double) => x / 2
  compressor(5)

  object helpfulCompressor {
    def apply(x: double) = x / 2
    val help = "compressor"
  }
  helpfulCompressor(5)
  helpfulCompressor.help
Ruby also has .call...but I can't remember it well enough


> Are there other languages that do this so nicely? It's the perfect blend of OO and functional.

In .net methods, properties, member variables, classes, etc. can have attached attributes that are objects. Attributes can be interrogated at runtime using reflection.

    [MyAttrib(foo=42, bar="baz")]
    class MyClass
    {
    }


I really dislike attributes in C#. Their use is a big code smell for me. They look like C#, but they're not actually -- they're a part of the type system that has been disguised. The fact that their parameters must be compile-time constants helps perpetuate a harmful "primitive-centric" viewpoint that is antithetical to many good design principals.

Their only reason to exist is to support a use case of a 1:1 relationship between classes and functional units (or methods and functional units, or parameters and functional units, etc.), which is almost always an 80% solution that makes the other 20% extremely hard and/or impossible. I would go so far as to say that every single use of an attribute is your own code begging you for a better design. Unfortunately you're sometimes stuck with them, but that's only because you're sometimes stuck with a framework that itself is begging its creators for a better design. I wonder if Attributes were never added to the language, if developers would have just made those better choices to begin with. From my vantage point, they were a clear mistake in a language that was otherwise very well designed.

They certainly are nothing like the ability to attach methods to functions. A better example of that kind of convenience in C# is extension methods, which you can certainly define on types like Func<T>. I love extension methods and miss them in every language that doesn't have them!


> Are there other languages that do this so nicely? It's the perfect blend of OO and functional.

In Lua, you have regular, non-object functions, but you can also create a callable table using metamethods, and keep whatever data you have with it. Add to that the colon syntax sugar (implicit self vs explicit self) on table methods and I think you have a beautiful "opt-in" OO story without the unintuitive `this` business from JS.


This looks like plain JavaScript.

What does TypeScript add in this scenario?

Does the example above even pass typecheck?


Yep. It's javascript, not typescript. Moreover, I think the post is only trying to convey that they like dynamic typing.


This is a Javascript feature, not really a typescript thing. You can do the same thing with python


If you take out all the “script” legacy (type coercion which was common for scripting languages when JS came out, the initial lack of a module system which led to all kinds of hacks, the scope of var declaration, etc), JavaScript and its prototype based approach is really good.

In fact I wish that constructor functions, and the class keyword never existed.

You can do the same with Object.create and a function closure, isn’t more verbose and it fits better with the mixed functional/oop approach of the language.


This blend of functional and oo programming was pioneered by scala.


Scala also supports structural typing, even though it's a bit clunky and against its nature.

And advanced typelevel shenanigans you see in TS usually have their counterpart in Scala, especially in Scala 3's meta-programming features.


Lisp of course supported both decades earlier though :)


Sure. Everything is reducible to lisp. But even though lisp had the same capabilities decades earlier, scala is the language that brought them to the mainstream (which may well because of the historical accident that twitter's backend was rewritten in scala). I don't love scala but it has been hugely impactful.


The meaning of "mainstream" changes throughout the years. Remember that Common Lisp started as quite literally the XKCD joke of "14 competing standards", except it actually worked -- in the sense that this 15th competing standard killed all the other ones and gained widespread adoption in the Lisp community. Tons of vendors threw money and effort at the situation, and it all started as an ARPA manager's idea. Of course, we live in different times now... But Zetalisp is one fairly popular Lisp I can think of that had funcallable objects. Whether the idea was appreciated or preferred over alternatives is a separate thing entirely, of course. I would assume most people would use a simple let-over-lambda to achieve the same effect as funcallable objects.


Wasn’t it Scheme?


>>> Are there other languages that do this so nicely? It's the perfect blend of OO and functional.

Everything is an object in Ruby as well


I don't like that at all. it all seems to be too informally specified and requires divine knowledge to actually know how you're supposed to use the magic function properties.


I could be wrong, but can't Common Lisp do this?


the flexibility you describe seems to be coming from lisp (schema) that javascript was designed on top of initially


give me something with ts’s type system and without exceptions and try/catch. i know go’s error handing gets shit, but i really like the explicitness of it.

oh and burn npm to the ground please.


Is that really suprising? Typescript is yet another language that has, kicking and screaming, picked up most of the ML featureset. I'd expect it to be, well, fine; the lack of real pattern matching is a pain, so it's going to be inferior to OCaml, but fine, no different from using C# or Swift or Dart or Kotlin or something of that ilk.


That's some very high level view - in reality even tough those languages are in similar categories the experience would be vastly different :

TypeScript - powerful type system but shit underlying stdlib and language (no pattern matching/switch expressions)

Dart - worse than TS because the object model is closed - so no dynamic freedom, but the type system and expressions are weaker then the rest. Also 0 meta programming facilities - Java level of boilerplate and code generators

C# - closest to ML featureset out of the mentioned, but unlike TS doesn't have sum types which will make a lot of things more tedious.


Your understanding of Dart sounds a little outdated. We have a fully sound static type system and the language is pretty expressive, especially with the new pattern matching stuff in 3.0:

https://medium.com/dartlang/dart-3-1-a-retrospective-on-func...


If you’re going to use C# for a compiler, why not go the whole hog & use F#?


No argument from me, just saying lumping in all those languages together sounds reasonable in theory, in practice they are very far apart.

You'll probably have similar overlap between C# and F# implementations as you would with say TypeScript - they are just that different in practice IMO.


I was going to mention F#. The parent and grandparent comments mention C# and ML, and the intersection of the two is... F#.


> no pattern matching/switch expressions

They're still waiting on the do expression proposal for that (https://github.com/tc39/proposal-do-expressions), which has been in the bikeshedding stage for the past five years.


The proposal for pattern matching syntax seems more akin to what they're looking for.

https://github.com/tc39/proposal-pattern-matching


I’m not clear on the benefits of this proposal over just using anonymous functions. Some of the examples just seem contrived.

And the React example makes no sense, you can use a ternary and it is even shorter.

``` return ( <nav> <Home /> {loggedIn ? <LogoutButton /> : <LoginButton /> } </nav> ) ```


Having written quite a lot of Reason react (similar in syntax to the proposed React example) - while in this particular case because the example is so simple the ternary looks nicer, its also nice to just have a longer expression block in the middle of your JSX when you're doing more complex things.


for me the point is to be able to return from inside multiple expressions from the correct scope i.e. the root function without throwing.


What if you want a `switch` instead?


Could you speak more to the dart, and specifically the "Java level of boilerplate and code generators"?

I've used it a bit but I haven't found it very boilerplatey in general, so I'm interested in learning what contexts you run into that.

I'm assuming you're using it with flutter?


Some crude snippets from a Pascal-C (I never could decide) self-educational compiler/toy I wrote a while ago (in Haskell):

    -- a parser function that matches on { ... } returning ... 
    -- can be combined with other parsers 
    braces = (symbol "{") `between` (symbol "}") 
    ...
    -- a statement is a controlFlow or a declaration or an assignment
    statement = controlFlow <|> declaration <|> assignment

    -- a block is either 1 or more statements surrounded by { ... }
    -- or a single statement.
    block = (braces $ many statement) <|> statement
    ...
    -- An expression is a number, or a variable, or a binary operation
    -- derive functionality to print and test equality of these values.
    data Expr = 
        Num Int
      | Var Variable
      | DualOp Op Expr Expr
      ...
      deriving (Show, Eq)
    ...
    foldConstants (DualOp Add (Num a) (Num b)) = Num (a + b)
    ...
Parser combinators (parser functions that take other parser functions and return more complex combined parser functions) with enough syntactic sugar can express BNF-like grammars directly as code. And ML-style list and pattern matching operations are very expressive for operating on the intermediate representation.


I'm quite fond of this library for pattern matching; it's a staple in all my new projects.

https://github.com/gvergnaud/ts-pattern


> picked up most of the ML featureset

It really hasn't. In this very post he had to use a visitor to work around the fact that switch isn't an expression.

JavaScript's support for iterators is also weirdly shit. It has `.map()` but that only works on arrays. You can't map an iterator!


You can now! The iterator helpers proposal is stage 3 and shipping in Chrome.

  (new Set([0, 1, 2])).values().map(x => x + 1).toArray()
You can also reduce, filter, find, etc.


I think there’s an iterator proposal out there for JS with generic map, filter, etc.


I've done a bit of typescript and kotlin-js. It always strikes me how close those two languages are. Yes there are lots of differences but they aren't that different and you can transition from one to the other pretty easily. I have my preferences (kotlin) but I can work with both.

IMHO typescript could just cut loose from its javascript compatibility. Why not compile it to wasm instead of transpiling it to javascript? Kotlin is in the process of adding a wasm compiler and they already have a js transpiler. The same code results in a lot smaller binaries and loads a lot faster in wasm form. Browser Javascript is not a great compilation target. And who cares what the minified crap that loads in the browser looks like? The only reason for backwards compatibility is allowing javascript projects to easily transition to typescript. But that's increasingly less relevant now that a lot of projects start out being typescript from day 1.

Of course, Assembly script is already a thing. But why not make that a bit more official? It wouldn't surprise me to see people doing a lot of web development in languages like that in a few years.


Typescript is defined to run exactly as the equivalent Javascript you get when all the type declarations are stripped out of the source. There's no benefit in compiling it to WASM unless you had a WASM JS engine (or JS->WASM converter) that executed the JS faster than the browser's built-in JS engine (or created WASM binaries that were smaller than compressed minified JS, which seems extra unlikely when you'd have to include a JS runtime). If that existed for general JS, browsers would just upgrade to use that internally on all JS.

Well, you could in theory get benefits by having optimizations based on the type declarations. This could be awkward to do when Typescript allows zero-runtime-cost interop with untyped JS anywhere and allows any value to be cast through the `any` type and then to any other type. If Typescript with these optimizations is still intended to execute in the same way as its untyped JS equivalent, then the optimizations all need to handle being opted out of when an unexpected value is received, in the same way optimizing runtime-type-tracking JS engines have to be able to abort out of their type-based optimizations. This optimization would be equivalent to pre-warming a JS engine's knowledge of runtime type information, which is an optimization that could be done for JS in general rather than by doing it just in the Typescript project through compiling to WASM.


Your parent suggested to make changes to TS so that TS is no longer compatible with JS (in the sense you described). Once that happens, compiling to WASM instead of transpiling to JS is a very valid design choice.


That's fair, though there have been several projects that have attempted to be "JS-ish but with some behavior changes with strict type handling sprinkled in for optimizations" like the Strong types proposal (cancelled) and Dart (which switched to being a compile to JS language just like Typescript), so I'm currently convinced that the trade-offs aren't obviously worth it and that it's unlikely Typescript will change its priorities in that direction in the near future.


The whole purpose of TS is better JS.


I'm afraid that's wishful thinking. JS compatibility is core to the TypeScript ethos, regardless of TypeScript's own popularity.

Besides, enums (?) aside, and ignoring typechecking, compiling TS is really easy right now. Switching to WASM is a high ask.


JS compatibility is core to Typescript's own popularity.


"Modern" Javascript essentially is Typescript without the type hints (e.g. if you ignore the historical baggage, JS is actually a fairly decent language).


Yeah, having just jumped into ts after a long js hiatus since back when The Good Parts was still surprising, it's quite awesome to see how much of what I assumed to be "ts stuff" is actually just postdeluvian js. Makes ts more attractive, not less. I do wonder however of it was possible to identify parts of that language superset that are fully redundant (as in not even required for exotic edge cases) and let loose some draconian linter?



Sure, but that's still seems quite mix and match, anything goes, choose your own adventure. There are many positives about this approach, but as a learner a little more common ground between codebases would certainly be appreciated.


True. I always thought that we should have additional mode stricter than https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe....


I actually argue JS was a better language before all of the changes made, starting with const/let. The only thing I'd say makes sense are classes but the fact they aren't syntax sugar over prototypes was a mistake.

People wanted a different language, they should have gotten more scripting languages in the browser. Not changing JavaScript so much that it's no longer JavaScript.


Coming into web development fairly recently (after 2013 or so) from the statically typed hemisphere and without closely following the Javascript history from the start I naturally cannot agree ;)

(things like the scoping rules for 'var' is just bizarre, same with the concept of 'prototypes')

Looking at typical "old-school" Javascript code gives me the creeps the same way as looking at K&R C code ;)


> aren't syntax sugar

They aren't syntactic sugar? Pretty sure `class Foo { blah() {} }` is equivalent to `function Foo() {}; Foo.prototype.blah = function() {}`.


There are a few very specific differences. In a subclass, 'this' doesn't exist until you call "super" for example, and the constructor will throw an error of invoked without the "new" keyword. [O]

These differences let you extend built-in things that simply can't be done with old prototype constructor syntax. [1]

[O] I should probably be using a more recent reference like MDN, but the 2ality series always sticks out in my mind whenever "just syntax sugar" comes up: https://2ality.com/2015/02/es6-classes-final.html#safety-che...

[1] https://2ality.com/2015/02/es6-classes-final.html#subclassin...


Thanks. It sounds like subclassing built-ins is where transpilers hit a hard wall. Other syntactic features of es6 can be transpiled.


http://kangax.github.io/compat-table/es6/

This page lists features from es6 (and newer versions linked at the top) along with compliance to the spec. First column is the current browser, second is babel+corejs polyfills.

Overall, babel gets about 70% of the way there.


The result of `let x = Foo()` when Foo is defined as a function is whatever Foo's return value is. Trying `let y = Foo()` when Foo is defined as a class throws a TypeError.


They are not, no.


My example is incorrect because it doesn’t cover all the edge cases but you can desugar es6 to es5, except in es5 you can’t subclass builtins.


> the fact they aren't syntax sugar over prototypes was a mistake

Totally different meaning.


What else would 'syntax sugar over prototypes' mean?


you can still write the old javascript. it'll feel very lonely, though.


> IMHO typescript could just cut loose from its javascript compatibility. Why not compile it to wasm instead of transpiling it to javascript?

My fantasy for the past year has been, if I could magically program anything, bringing a compiler and spec wholesale into the world out of the void, I would create a new language (call it WebScript as a placeholder) that

- featured an ML style type system, ADTs, a type syntax nearly indistinguishable from TypeScript

- whose actual core language essentially resembled Kotlin or "Go with exceptions"

- compiled to WASM or JS as a compatibility bridge

Nothing radical. Nothing revolutionary. Just these things would be an immediate plus.

But maybe AssemblyScript is a good enough step towards that.


I’m actually working on a language like this. I quite liked ReScript/ReasonML, but having to manually write binding to use TypeScript or JS code is a drag. I’m making a functional language that looks and feels like TS and lets you import TS directly. Mostly just stripping imperative statements, removing class declaration and adding pattern matching and better data constructors (never liked the discriminated unions). WASM as a target is a bit further off.


Go has exceptions: you can use `panic` to throw/catch just fine. The community will bring out the torch and pitchforks because it’s “not idiomatic” but if you’re programming solo or with other pragmatists don’t let it stop you.


Does the community care if it’s not in a public API? If it’s caught within the same library it doesn’t affect anyone else.


Or just pass pointers because you need optionality, and get null pointer exceptions for free :)


I've been hoping to see alternative targets to JS for TS as well. WASM makes a lot of sense but TypeScriptToLua¹ also looks interesting.

1: https://typescripttolua.github.io


> IMHO typescript could just cut loose from its javascript compatibility. Why not compile it to wasm instead of transpiling it to javascript

In browsers the Wasm runtime doesn't have access to the DOM APIs. So that's wishful thinking ATM.


The biggest expressivity pain point in practice for me is try/catch not being an expression, so I can’t do

   const c = try { … }


Yes! This just drives me crazy:

   string something = null;
   try {
      something = mayThrow();
   } catch (SomeException ex) {
      log.Info(ex);
   } catch (Exception ex) {
      log.Fatal(ex);
      throw;
   }
   if (something != null) {
      ...
   }
Would be actually nice if it had F#'s try...with https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref... and would allow us something like:

   var something = try {
     mayThrow()
   } with (SomeException ex) {
      log.Info(ex);
   } with (Exception ex) {
      log.Fatal(ex);
      throw;
   }
Or some way to do pattern matching on exceptions for switch expression.

   var something = mayThrow() switch {
     (SomeException ex) => {
       log.Info(ex);
       return null;
     }
     (Exception ex) => {
       log.Fatal(ex);
       throw;
     }
     var passThru => passThru
   }


   var something = (() => {
     try {
       return mayThrow()
     } catch(SomeException ex) {
       log.Info(ex);
       return null;
     } catch(Exception ex) {
       log.Fatal(ex);
       throw;
     }
   })();
Close enough?


not really; can't immediately return from the lambda caller without throwing, always have to either try/catch anyway or if/else the return value of the lambda. (also, it's rather ugly to read and inconvenient to write.)


Another idea, introduce ??? that would invoke on exception :)

   var something = (mayThrow()
      ??? (SomeException ex) => { log.Info(ex); return null} )
      ??? (Exception ex) => { log.Fatal(ex); throw; };
Or maybe just start using Results as return type and get ValueOrDefault :) But when it comes to handling exceptions, I think it explodes there with ifs and processing IENumerables: https://github.com/altmann/FluentResults

But then again, simpler Results wrapper may be used perhaps. But it is a different way of coding and takes some mental shift on how to think about errors and distinguish between error results and true exceptions.

Oh, csharplang already had discussion on the matters about try/catch single statements: https://github.com/dotnet/csharplang/issues/786


> can't immediately return from the lambda caller without throwing

You want to immediately return in the middle of something that was supposed to simulate just evaluating an expression?

You can't immediately return in the middle of 2+2 as well because return is a statement and can't be a part of expression.


I never ever use try/catch in my code. The only time its really necessary is to wrap JSON.parse on use of untrusted input. For everything else it just feels sloppy as through there is insufficient logic in place for handling primary conditions versus edge cases. Also, try/catch will never compile in the JIT.


> Also, try/catch will never compile in the JIT

This was only ever true in V8, and hasn't been the case since 2017 with the optimizing compiler switch from Crankshaft to TurboFan.

Take a break before lecturing commenters on "guessing about performance is perhaps the most frequent anti-pattern in all programming" next time :)


> Also, try/catch will never compile in the JIT

Changing the way you program to fit what a JS compiler does or doesn't do is a fool's errand, IMO. The performance benefits are likely to be minimal, confusion for anyone else who has to touch the codebase is high.


That depends on how many changes it requires. If its just a matter of don't do these 3 things and your code suddenly becomes more predictable its like being slapped with a magic wand. Everybody wins. All you have to do to ensure 100% of your code compiles in a JIT is be predictable. Predictable code is, on its face, always less confusing.

> The performance benefits are likely to be minimal

This makes me cry, but not a cry of joy or ecstasy. People guessing about performance is perhaps the most frequent anti-pattern in all programming. Please read the following document, you can skip to the end but it may not make much sense if you do.

https://github.com/prettydiff/wisdom/blob/master/JavaScript_...

When developers make random performance assumptions, defensive assumptions are worse, it immediately identifies a lack of professional maturity. Its like small children playing games that expand their imagination, which is great for small children but less great for developers pretending to be adults.


> People guessing about performance is perhaps the most frequent anti-pattern in all programming

I disagree, premature optimisation is.

Changing your style of programming to suit what todays JIT does but tomorrow’s may not, in anticipation of performance issues you have not yet encountered is a waste of everyone’s time. No doubt it is valuable in extremely performance sensitive code but that is the extreme minority, especially in the JavaScript world.

If I were involved in a project where performance concerns were high enough to require everyone know what the JIT is and is not doing my proposal would be to use a language other than JavaScript. If thinking this makes me a “small child” in your mind I’m fine with that.


What you are advocating, the guessing about performance, is the premature optimization. Don't feel bad, most developers have no idea what that term really means. Here is the original essay where it comes from: http://web.archive.org/web/20130731202547/http://pplab.snu.a...

Premature optimization is the extra effort required to alter work necessary to circumvent guessed optimization pitfalls. In the same breath Knuth advocates always targeting known performance advantages.


> Don't feel bad, most developers have no idea

Some advice I know you’ll ignore: your tone in the comments here is deeply patronising. You know absolutely nothing about me and yet are entirely comfortable dismissing my perspective as wrong simply because yours must be correct. It’s not an interesting or rewarding way to converse, it makes me want to stop talking to you as soon as I can. Which I’ll be doing here. Have a good weekend.


Yes, I work in a language where junior developers and people deeply unaware of the language advocate anti-patterns and bad practice as matters of law, often for personal defensive reasons. Its hard to not feel patronized. You are correct in that I do not know you, but I have been doing this work long enough to see when people hide behind abstractions they don't understand as necessary to mask their level of confidence and then argue from for best practices from that perspective.

My best possible free advise: only advocate to what you practice. If, for example, you have never written an application in JavaScript without a framework, such as Angular, then you are an Angular developer not a JavaScript developer. If you speak to something you have never practiced with a presence of authority people with 10, 15, or 20 years experience will be condescending. That is why imposter syndrome is a thing. Its better to get the unexpected honesty from some irrelevant guy online than get hired into a job and either get it in person or worse compensating for a manager with imposter syndrome.


This is exactly what I’m talking about. You are assuming I am inexperienced and have no idea what I’m talking about because I have a different view to you.

For what it’s worth, I’ve spent years working with the innards of various JS engines, managed deployments of JavaScriptCore inside iOS apps (fun fact: no JIT) and using QuickJS in low resource environments (no JIT there either). There’s an interesting conversation to be had around optimising your code for JIT, given the different environments we’ve both worked in. But your ego won’t allow it to take place. A shame for all concerned but in my years of development I’ve met plenty like you and I’m well aware that I’m not about to change your mind so I’ll just leave it there.


And, JS engine's whims change frequently enough that yesterdays micro-optimisation is today's deopt.

Much better to just write ideomatic code.


For everything with the slightest bit of I/O it is quite practical if you have even the most trivial assumptions about its form.

Sure, you can check for everything. It often is more effective, but not prettier or easier to read and of course you also will miss cases.


This sounds like some hyperbolic framework nonsense. First of all I/O just means access to an API. I am going to presume you mean messages from a network.

In that case a message from a network either sends or fails and in the case of sending try/catch does nothing for you but provide an error object, and you don't need try/catch to capture the error. In the case of receiving a network message the message comes in or it doesn't. If the message does come in and is malformed your application should be mature enough to handle that appropriately to the benefit of the user instead of crapping out some error messaging to the console. You don't need try/catch to do any of this. A simple if condition is more than sufficient.

> check for everything

You should check for everything. You only need to check for one thing and if you check for it you are already at 100% of everything. Did you get what you expected: Yes/No?


Disagree.

Usually you don't need to just know if there is an error or not -- sometimes you need to know what that error is. For example, an I/O error versus your image-is-too-small error -- you need to respond to the user differently. I've seen plenty of web apps that treat I/O errors and user input errors as a generic "an error happened" and that is completely wrong. Proper exception bubbling means none of that code that encounters the errors needs to necessarily know how to handle the error and your app always responds correctly.

That said, I don't use exceptions as much in JS because exception handling in JS is still very nascent. There's no base library of exceptions and telling different exceptions apart is not built-in to the language. In a language with more mature exception handling, it's a breeze.


You made the same mistake as the grandparent by assuming I/O means something more specific than it does. At any rate there isn't a good reason to bubble error objects out of the event handler and secondly even if you did you would have everything you need to fully describe the problem within the error object including the stack trace, event type, error message, and so forth.

Its an equivalent breeze in JavaScript as well unless you are fumbling through abstraction layers (frameworks) that eliminate the information you need.


Not at all. You don't have to describe anything when throwing an error because that's the point of exception inheritance. Second, the whole point of exceptions is to bubble them out of the handler(!) because otherwise you would use return values (like in Go).

Since you are saying that you have to describe the problem within the error object, it sounds like you must be you are parsing error messages and stack traces to figure out what the error is. That's not how exceptions are to be used and I think that's why you are not seeing why you would bubble errors.

In other languages, you don't have to do any of that nonsense because object identity is much stronger, but JavaScript uses prototypical inheritance so you can't really tell if an error is an ImageError or an IOError. Despite this issue, exceptions were added to the JavaScript language without resolving that problem. The reason why you have to worry about frameworks eliminating error information is because that problem wasn't resolved and so frameworks roll their own error handling and there is no standard.


> Also, try/catch will never compile in the JIT.

If this is an actual concern (as in you have measured and try/catch is actually affecting something performance sensitive): you can most likely mitigate the impact by isolating the try/catch bodies in separate functions.

I haven’t verified this in every JIT, but I saw meaningful perf improvement in V8 for real world degenerate cases where the performance did actually matter and make a significant difference. But seriously, as always, measure before optimizing stuff like this! It might matter, but it seldom will for anything more than academic curiosity.


Perhaps the place I use it most is that try/catch is how you deal with rejected promises that you've `await`ed in an `async` function.


> Also, try/catch will never compile in the JIT

That's false. It used to be this way with Crankshaft but since 2017 we have Turbofan which solves this.


With async code, there is

    const c = myAsyncFun()
               .catch(e => …)


That leaves c as Promise which was not the type they were looking for. You'd still have to unwrap the promise and deal with the result or rejection. The easiest way to do that is to use await, and if you are using await you'd be better off using try {} catch {} around the async function than .catch().


What does a try expression like this do? Returns null/undefined on a throw?

I suspect you could do something like:

    const c = attempt(() => ... );
where attempt invokes the lambda, catches any exceptions, and does what you want


> What does a try expression like this do? Returns null/undefined on a throw?

If an exception is caught, the expression evaluates to what the catch block evaluates to. (Similar to having if/else be an expression). Of course if the exception isn't caught then it propagates so the expression doesn't take a value.


Kotlin has this feature (`try` and `if` are expressions not statements), and a good model for return values. https://kotlinlang.org/docs/exceptions.html#exception-classe...


I use iife for stuff like this.

const c = (

    ()=> {
        try: {...}
    } 
)()


Zig can though:

  const std = @import("std");

  pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
  }


that's not using `try` as an expression. can you do `let foo = try <code> (plus some handling for diverging in the other case?)`


It can and you in fact either have to mark that you return that error when you use try, or you have to provide a default value in a catch block:

  fn potentiallyErrorReturningFunc() !u64 {
    ...
  }

  const foo = potentiallyErrorReturningFunc() catch { 0 };

One notable difference to most other languages is it being a value only, basically a product type of return value XOR error. These error values get serialized into a number by the compiler that is unique, and on the type system level you deal with sets of these error values. Quite clever in case of a low-level system, in my opinion.


> const foo = potentiallyErrorReturningFunc() catch { 0 };

Nitpick, but AFAIK this won't work and needs to be rewritten either to:

    const foo = potentiallyErrorReturningFunc() catch 0;
...or to:

    const foo = potentiallyErrorReturningFunc() catch blk: { break :blk 0; }
(I wish there would be a more convenient way to return a value from a scope block, not necessarily with a "dangling expression" like Rust, but maybe at least getting rid of the label somehow)


Yes, 'try' can be used like an expression in Zig which resolves to the unwrapped success-result of an error-union, or in case of an error immediately passes the error-result up to the calling function.


Would you not just move that `try { ... }` block into a separate function or method?



I would think C# is easier for this due to having proper (and very convenient) pattern matching.


C# doesn't have anything resembling proper pattern matching. C#s pattern matching is akin to a marginally improved switch or if/else-if chain. C#s pattern matches aren't exhaustive, and the compiler doesn't properly check them at all.[0]

There are no proper sum types in C#, so 80% of the point isn't even there.

    enum Season
        {
        Spring,
        Summer,
        Autumn,
        Winter
    }
     
...

    int PatternMatch(Season season) =>
    season switch {
        Season.Spring => 1,
        Season.Summer => 2,
        Season.Autumn => 3,
        Season.Winter => 4,
        // compiler can't prove the above code is exhaustive because no proper sum types
        // compiler needs nonsensical branch here that diverges
        _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
    };


>compiler can't prove the above code is exhaustive because no proper sum types

Thats because c#s enum is not "closed"

https://github.com/dotnet/csharplang/issues/3179


These things are not about pattern matching but about other language features.


Lack of pattern matching is annoying, but Typescript's tagged unions and type narrowing from if-statements are a good substitude and make for a very pleasent time traversing ASTs. C# has it's own way to express similar data structures (with subclasses and interfaces), but I find unions+narrowing much more natural in Typescript.


Type-refining if in TypeScript is good enough for pattern matching if you ask me.


The TypeScript type system meta programming actually does pattern matching very well, it’s just the underlying JS runtimes that can’t, there is a proposal for it though; https://github.com/tc39/proposal-pattern-matching.


This is why once WebAssembly more polished and more common, languages like JS and TS could become obsolete since proper, more powerful languages will suddenly become a valid choice for Web.


What language would that be?

I've programmed in Java, Python, C, C#, TypeScript, VHDL, and some other languages and they all fucking suck in some way.

If it was up to me, I would pick the best parts of the languages/ecosystems and make a Frankenstein language.

And anyway in the end, it's never the language. It's the ecosystem. Just because you can run Java on .NET doesn't mean you should -- because almost no one else does it your way and it's not worth the trouble maintaining some weird approach very few people use. You're never going to get help from library authors when it doesn't work on your weird setup.


> This is why once WebAssembly more polished and more common, languages like JS and TS could become obsolete since proper, more powerful languages will suddenly become a valid choice for Web.

JavaScript and TypeScript are not "proper" languages?

Different languages are powerful in different use case dimensions (tradeoffs), and the typical client-side web programming has a very very strong UI angle, which many "more powerful" languages that I am going to guess you have in mind are less adept at.


JavaScript's redeeming quality is that it is ubiquitous due to being used by browsers. I don't think anybody credibly claims it to be a language we would want to use if we were to redesign web programming from scratch today


What follows is meant to be descriptive rather arguing "my camp is right".

> JavaScript's redeeming quality is that it is ubiquitous due to being used by browsers.

JavaScript has other properties that make it very adept at a wide range of web app usecases. Languages exist not in isolation, but have a dynamic environment w.r.t. domains they excel in, tooling, mindshare, programming in the small and the large, comlexity, etc.

> I don't think anybody credibly claims it to be a language we would want to use if we were to redesign web programming from scratch today

Dart has tried. Many languages transpile to JavaScript (and have for a long time).

Yet, JavaScript and TypeScript are some of the most popular languages on the planet.

If the web is still around in 20 years, it will be interesting to see whether another language has taken center stage. I'm not quite holding my breath, if only because even with transpilation being around for many many years, no other language has overtaken. I have a hard time seeing WASM changing that outcome in a meaningful way.


> I don't think anybody credibly claims it to be a language we would want to use

Ouch, you posted this on the internet.

Somebody, somewhere is certainly going to claim this. Some of the people with that claim are even going to have a rationale behind it.

And to be fair, the tooling is going to push a lot of opinion. Because as bad as the JS/TS tooling is, its UI component has been evolving steadily while every other tool stagnated. And by now it has quite probably surpassed the 90's RAD paradigm (I mean, I can't make up my mind).

IMO, the language is one of the main things holding those tools back, but most people just won't agree.


Outside of the "everyone hates javascript" circle jerk, I really enjoy writing Typescript. I find its type system to be very natural and pleasent to use. It's my go-to for any generic task.


TypeScript is a pleasure to use. I have no desire to replace it with another language.


Any non-toy WASM application running in browsers will almost certainly end up as a hybrid WASM/JS application, just because you can't call directly from WASM into browser APIs, and even if the JS shim is "hidden from view", sometimes it's either more convenient or even more performant to write more than just a thin API shim in Javascript, especially for asynchronous code or to avoid redundant copying of large data blobs in and out of the WASM heap.


> just because you can't call directly from WASM into browser APIs

Surely that will change someday.


You are still limited by the fact that browser APIs are mainly designed for usage with Javascript, and those Javascript APIs usually don't map all that well to languages used with WASM (which typically use the "C ABI" for FFI). There is zero chance that each web API will get a second API which is compatible with "C calling conventions".

There is a (now inactive) "interface-types" proposal, which has dissolved into "component-types", but this scope-creep definitely is already quite concerning:

https://github.com/WebAssembly/interface-types


You mean, like C and its derivatives became obsolete once successors to PDP machines were built? ;-)


Naively, I would have guessed it being harder than C#


C# and Swift have pattern matching.


It's more limited than what you'd ideally want: pattern matching in function definitions. This makes walking ASTs a breeze.

Edit: And inline pattern matching for values returned from expressions and function calls (similar to destructuring, but more powerful).


I would say what C#11 does is already quite good, likewise for Swift.

Perfection is the enemy from good.

Those I can use at work, ML derived languages not, even F# is an uphill battle in most shops.


Very much apropos:

Going between Rust and TS it is painfully obvious how much sth like tagged enums are missing, which can also be seen in this post.

I know of this [1] proposal for ADT enums which looks like it has stalled. Anyone know of other efforts?

[1] https://github.com/Jack-Works/proposal-enum/discussions/19


I find discriminated unions work ok for similar use cases.


TS's type system is fun but a part of me always wonders how much faster TS's compiler would be if it was written in a compiled language (assuming "good implementation", which is a big assumption!)


PS: swc and esbuild aren't good example, because most of the speed improvements comes from the fact that they are just stripping TS-specific syntaxes to generate JS code.

Also tsc is slow, sure, but only for the first run. Enabling `incremental` flag or using watch mode with `--transpile-only` usually brings compile time under 100ms, Making it practically indistinguishable from SWC or ESBuild.


There's an answer from TypeScript team for your question :) https://twitter.com/drosenwasser/status/1260723846534979584

Basically,

> Let's say TypeScript takes over 20 seconds to type-check a medium-sized program. That's not usually because it's JS, it's often because of types that cause a combinatorial explosion.

Also

> A different runtime can afford a lot (it sounds like parallelism and start-up time in this case) but I haven't seen a CPU-bound benchmark that supports the idea of a 20x all-up speed-up.


Both swc and esbuild claim to be much faster typescript compilers in part because they're written natively, but are actually just "cheating" and not doing any type-checking at all.

That's not to say they're useless though, they're good for fast hot-reload as you can have type-checking running in parallel.


it's easy to write a typed language compiler if you don't need to actually check types :)


I mean I get what Daniel's saying, but I kinda doubt there wouldn't be _some_ speedup. And hey, what if type checking for small programs went from half a second to 100 ms? That would be nice!


That's a five times speedup. I wouldn't expect a typical 'compiled' language to run five times faster than JavaScript on the kind of code you find in a compiler.


You are excluding startup time. Loading the interpreter/runtime and initial JIT pass is slow. For many codebases, the compilation time is actually dominated by the time it takes the interpreter to get going.


The type system is fun right up until you are using it to its full ability in generics..then you look at the 5 lines of 'type logic' you spent the last day debugging and ask yourself how you got here.


I've definitely seen it suck people into a black hole. It works best when you think of it as a form of documentation with a benefit of enforcement, rather than trying to use the type system to prevent every conceivable bug. Massive unreadable types don't help programmers write better code.


Pretty true lol chaining something together that ends up looking like this:

<<<<<<<<T>>>>>>>>

Is basically a guaranteed headache


Wonder no more: https://github.com/dudykr/stc

Written in Rust by the (lead?) dev of SWC

---

SWC (speedy web compiler) compiles TS to JS

STC (speedy type checker) checks TS types


Performance of the TypeScript compiler has greatly improved in the last versions. The future isolated declaration mode promises great improvements: until 75% of reduction in compilation time! [0]

[0] https://github.com/microsoft/TypeScript/pull/53463#issuecomm...


Probably not that much faster. V8 is very good at optimizing JS code. Typescript is slow mostly because of the complexity of its type system (which is required to make it backward compatible with existing JS code).


Probably quite a bit faster if you do not have to instantiate a new V8 for every single file, considering how many files your average npm project has. I'm not certain if any contemporary build systems reuse the compiler process for multiple files


Why would it have to instantiate a new V8 for every file? You can just run 'tsc' in your project dir to compile all Typescript files.


I think people often underestimate how much the TypeScript team does to make TypeScript fast and efficient. They have some of the best people in the field working on the type checker


Yes, because it's one of the slowest compilers in the world, and its job is to compile from JavaScript to JavaScript. There are other languages with complex type systems, where the compilers have to do a lot of other additional work on top of typechecking, and are still way faster than TS.

And TypeScript is always slow, not only when you do abuse its type system and require it to do complex inference - I mostly use it for "this is a number" and "this is a object with the following 4 fields" and it's still by far the slowest component in my builds usually


Indeed. I haven't checked the implementation, but I suspect that "don't spin up a separate process for every file" is an idea that has probably already occurred to them :D


I haven't used it in a couple of years (tbh I may have to remove "full stack" and "Angular" from my CV...) but I don't recall TS compilation being particularly slow. Are people not happy with how quick it is, or do you have a particularly big/complex application you're working with?


Notion is more than 10k typescript files and we view typecheck slowness and memory pressure as an existential threat to our codebase. Right now our typecheck needs ~12GB of heap size but the memory needs have accelerated recently.


NX likely would do magic for you all but I imagine it is far too late to do anything about that.

We had a very large Angular 10 project that we replaced with an NX Angular project late last year. We went from ~6 minute prod compile to around 20 seconds, recompile is lightning fast because it only builds your affected library. That is all without using the remote library cache feature where you can save compiled libraries to the cloud such that a user only builds the libraries they are changing ever.


Can you not split the 10k files into modules for incremental/parallel compilation?


There’s https://swc.rs/, a Rust implementation (albeit without type checking at this time).


Not really a fair comparison. Stripping out types is trivially fast regardless of the implementation language. The _vast_ majority of the time taken in tsc is because of the type checking


See the sibling comment https://news.ycombinator.com/item?id=37173313 for a type checker by the same author.


> part of me always wonders how much faster TS's compiler would be if it was written in a compiled language

TypeScript's compiler _is_ written in a compiled language.

(I think you are using "compiled" here as a euphemism—one that doesn't help anyone.)


The reason we need to buy new hardware every few years, and fill landfills with our old stuff, is because of ideas like “let’s use JavaScript to write a compiler!”


No lie, the M1 was a game changer for large Typescript projects.


It sure is. For anyone looking into Compilers and just starting out, I recommend this book: https://keleshev.com/compiling-to-assembly-from-scratch/

The author uses a TypeScript subset to write a compiler to 32bit ARM assembly and explains that it almost looks like Pseudocode, so it is very accessible. A sentiment I can get behind, despite avoiding it in any case possible.


I would also _highly_ recommend Crafting Interpreters: https://craftinginterpreters.com/

The book is split into two parts. The first implements a language interpreter in Java, and the second implements the same language by building a compiler to byte-code and also implements a byte-code VM in C. Every line of code in the implementation is referenced in the book.


Thanks for the ref. Looks very interesting!


Anecdotally, I love the reference to the classic "Dragon Book" that this cover makes.


This looks great -- thank you for sharing!


To OP: You could avoid the visitor by using an IIFE style switch using the run utility fn:

    export const run = <T>(f: () => T): T => f();
Now you can go:

    const inferred_type = run(() => {
      switch(blah) {
        ...
      }
    })


Exactly! I wrote a post about this pattern: https://maxgreenwald.me/blog/do-more-with-run


Yeah this is where I heard about it :P I showed all my dev friends, and our minds were equally blown away by the obviousness of it.


You don't even need to do this much, you can just invoke the function and let the return type be inferred.

  const inferred = (() => "Hello")() // Inferred as "string"

If you really don't want to use an IIFE because you don't like the "()" at the end, you can just:

  const myFn = () => { switch(...) }
  const inferred = myFn()


As some how is writing a compilter in TS, I agree it's not too bad. I started with Deno like the author but ended up switching to Bun which, despite some rough edges, I'm enjoying more than Deno and it's _very_ quick! (My main niggle with Bun is the test reporting, but it's getting the job done)

For standard parser generator frontend, Ohm-js[1] is quite pleasant. I wouldn't recommend anyone reviews the offical tsc compiler, it's huuuge - instead there's mini-typescript[2] (in particular the centi-typescript branch[3]) which does a nice job illustrating how tsc is working.

[1] https://ohmjs.org/ [2] https://github.com/sandersn/mini-typescript/ [3] https://github.com/sandersn/mini-typescript/tree/centi-types...

Looking forward to GC an DOM access in WASM.


Curious. What made you switch to Bun?


I know this is probably blasphemy at this point, but I actually wasn't enjoying package management in Deno (or lack thereof). I've been an avid user of the NPM ecosystem for a long time (10+ years), it's entrenched deep in me and I don't mind it haha! In particular with Deno, it was trying to remember all the dependency URLs, and not wanting to duplicate them - every time I wanted to read a file and I had to lookup the std library URL for the umpteenth time. The package discovery isn't as good, and documentation was too different (why is everything in a file called mod.ts? why do I need to see all of the files in a dependency?).

This led to the recommended advice of having a dedicated TS file where you pull all your external dependecies in and then re-export them locally. This felt worse than package.json (read: clunky not having a first-class system to list external dependencies). And this is in a project with minimal dependencies too - yet it still frustrated me. Plus my editor had no type info for any external imports. I then discovered import maps, but that was still hit and miss in my editor (Neovim mind you) with code completion, made worse by not having a clear way to import ohm-js. Not to mention having to explicitly list `--import-map` for every command and forgetting too often and then wondering why my code wasn't working. Just feels like it's on a roundabout path to reinventing package.json anyway, just with distributed packaging by default?

I'm not particularly concerned with security either, so having to list `--allow-x` and `--allow-y` got tiring very quick. At this point, I just felt like Deno was constantly trying to stop me from getting stuff done. I ended basically ditch most of the new stuff in Deno and was basically treating it a ts-node (thanks to the new NPM/Node compat feature) - but it had me off side now.

I knew Bun.sh has some rough edges in terms of compatibility, etc (which is fine as it's pre-v1)... but for writing a compiler: I'm mostly writing everything from scratch and just need to read/write from the filesystem which is definitely stable and well documented. Switching to Bun was painless and all of a sudden everything Just Worked™, ran faster and I can get back to actually working on my compiler. The only problem I've had with Bun is not being able to tweak the output the results of `bun test` (e.g. if one of the first tests fail, I have to keep scrolling back up through my big list of passing tests to see why it failed) - if Bun had a better error summary at the end of the test output I'd be set!

TLDR; As someone using NPM/node for over a decade, nothing in Deno "just worked" for me and instead blocked me at every turn and the lack of package management turned me off. I know there are sound and objective reasons for design decisions in Deno, but none of those things matter much _to me_. Bun, on the other hand, is intended as a drop-in replacement for Node, but faster and with native support for TypeScript which ticks all the boxes I need for this particular project. (FWIW I tried switching a Remix project to Bun and it didn't work and stuck with Node).


Interesting, I'll really have to dig deeper into Bun. I know I've been having a hard time switching to anything else because I rely heavily on a typescript compiler transformer and that hasn't wanted to transfer to well to other things (like from Webpack to esbuild for example).


Wow, nice summary. Exactly what I feel about Deno although Deno has standard tooling for formatting/linting to make the story a bit better.


That is surprising, I would've thought that TS would have more overhead because of the interfaces adding extra weight. Makes me wonder what else TS could apply to. Language parsing, maybe?


At runtime, interfaces have zero weight. They get completely compiled out.


The result shouldn't be all that surprising considering the TypeScript compiler is written in...TypeScript. So TypeScript as a ML is already battle tested and is in heavy production use every day.


Ive been writing compiler in C# and I dont see anything fancy here except union type

Ive personally decided to avoid visitor pattern bloat and Im waiting for closed enum feature in order to have compile time exhaustive check


I find myself really missing union types when I'm in a language that doesn't have either it or Rust-style enums. The usual alternatives are awkward: either one class representing the union type containing N nullable properties with the documented condition that only one property is ever expected to be non-null, a condition the type system isn't able to check or make use of, or multiple classes extending a common class, which feels really heavyweight. Both solutions require some duplication or clever composition if you want several different overlapping union types.


I really don't know what the author means by "language-centric" vs "implementation-centric" and "production-ready"...


language-centric: the language you are compiling is flexible (you are also the language designer), you want to build the best possible language

implementation-centric: the language you are compiling is mostly fixed and/or defined by an external entity. You want to build the fastest compiler for this language, which emits the fastest code.


Can I humbly add "Anders Hejlsberg" to the list of reasons why none of the commenters seem surprised?


Not that surprising: https://news.ycombinator.com/item?id=37039443

Of course, that one has been downvoted to -4 (as of now).


It’s just not the right tool for the job .


It's the perfect tool for the job.


If all you have is a hammer, everything begins to look like a nail.


Why isn't it the best?


every day we stray further from God


I'm sick of seeing overly complicated TypeScript code. Mainly overly complicated types.


compilers are really complicated pieces of software. There's a lot of complexity even in small routines. I would expect to encounter complicated types in there, they capture (while complex) intent and meaning.

It's not the frontend glue-code kind of code. The inherently hard problem makes is fun for people who like to solve small puzzles.


As the post nicely demonstrates, TypeScript is definitely not OK for compilers (and not surprising at all!)

It doesn't even have destructuring pattern matching!

At this point, even Java is better [1].

[1] https://github.com/tomprimozic/caya/blob/master/src/caya/Int...


Pattern matching is definitely not a requirement for a language to be good for writing compilers in.


With how powerful the type system is you can implement pattern matching via a library pretty convincingly, https://github.com/gvergnaud/ts-pattern is definitely the go-to. That being said pattern matching is hardly a requirement for being ok for implementing compilers.


Gut reaction is to use LLVM for everything...


What does that have to do with the compiler's implementation language? A written-in-TypeScript compiler can emit LLVM bytecode just like a compiler written in any other language.


it isn't, i have codebase in golang that is much larger than typescript and it is pleasant to work with lsp and compiles smoothly. With typescript i have to turn off lsp and even after that it takes long time too compile. There is a reason why people are writing typescript compiler in rust.


He's talking about compilers for small languages. The Typescript LSP works fine on very big projects like VSCode so I think you'd need an enormous language like C++ or Rust before you'd run into those limits.

But still, I think I'd rather use Rust. I'm pretty sure the code would be nicer (e.g. no need for the explicit tag field or for the visitor hack).


The author is no stranger to rust (he’s creator of rust-analyser). The reason why he’s pitching typescript here is due to its high level nature and doesnt have to deal with memory management, low level integer types etc.


I know. I'm just expressing my opinion that he hasn't really made me want to use Typescript over Rust for this case.

I like Typescript and Deno. But I'd still rather use Rust here.

I would love it someone made a RustScript though which would be basically Rust but with arbitrary precision integers, garbage collection etc. (but still value semantics).


I see ‘TS server has been restarted 5 times in 5 minutes’ daily.


I don’t see this and I work on large enterprise websocket server with almost a million daily active users. In fact, I think we had 100% up time for the last 12 months except for a few AWS outages. Codebase is 10 years old and was CoffeeScript -> ES6 -> TypeScript.


nono, I mean the LSP in vscode. prod deployment is unrelated.


But to what end? For a compiler, there is no need for all the overhead of npm (usually), ts, tsconfig, package.json bundler and bundler configuration, to get something usable, unless one really wants a JS thing at the end to run in the browser. I imagine even some webassembly tool chains may be shorter.


The article answers this question. The author is using deno which is a single binary that uses TypeScript without all of that.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: