Hacker News new | past | comments | ask | show | jobs | submit login

I get paid to write TypeScript, it has come a long way to a relatively enjoyable experience compared to how expressive i feel with Scala. Looking forward to giving Scala 3 a spin to learn whats new



As someone who has spent a lot of time diving deep into Scala (I worked on compiler related semantic tooling for scalameta, worked on a experimental parallelizable Scala compiler, and even have a single commit in this release from almost four years ago LOL), and who more recently has been working with TypeScript, I find this really interesting and agree in some ways.

Scala 2.x already had path-dependent types, which combined with implicits and type inference were technically sufficient to implement versions of of the dependent function types we have now in 3.x. In my opinion, how you did this previously could get really clunky (See e.g. hlist mapping in https://github.com/milessabin/shapeless/blob/8933ddb7af63b8b...). Lots of things that frankly felt like casting magic spells, and there were sometimes fragile and weird bugs especially with how implicits were resolved, see e.g. https://github.com/scala/scala/pull/6139.

The native type-level meta-programming coming in 3.x along with a lot of streamlining of the underlying type system and implicit resolution rules (e.g. https://github.com/lampepfl/dotty/pull/5925) could help a lot. This is just one example of a more advanced feature, but there are lots of things like this that changed in Dotty (maybe the most interesting is the DOT calculus that makes a better backbone for the type system). I don't know what tooling for Scala 3.x looks like, but even Intellij's frankly amazing Scala plugin broke down sometimes for 2.x.

After using TypeScript, I just personally enjoyed the experience better. Technically Typescript's type system is equally as powerful as Scala's, but the primitives for expressing more complicated types just seem much easier compared to the path-dependent stuff you would need to do in Scala. I was amazed to find stuff like this https://github.com/aws-amplify/amplify-js/blob/dedd5641dfcfc... in mainstream wide usage. I'm not even sure how you could write these type of transformations as well in any version of Scala. I suppose maybe records work, or in this specific case there are functional patterns that are preferred.

Caveat, I haven't followed Dotty for a couple years now, so I could be missing a lot. Regardless, I have really enjoyed the tooling around Typescript as well as the design. The real-world usage of more complicated dependent types (and there are good uses IMO) have been more fluent and easier to understand, and less dependent on tricky behavior like for implicits. VS Code's tools are really magical, and I hope with some of the amazing underlying work done in Dotty the Scala developer experience could look more like that.


I think TS's typesystem still stays stronger than Scala's in some areas (and vice versa). But I believe the links you posted (like https://github.com/aws-amplify/amplify-js/blob/dedd5641dfcfc...) can now done in Scala 3, mostly to the introduction of match types.

In general, Scala focusses a bit more on theoretical foundation and sound features (which makes progress slower), whereas TS focusses more on practicality. That's pretty exciting because it also means that TS leads to more "experimentation" and if something works out well (and can be proven to scale into the future and interops well with other features) then it can be added to Scala in a more general/abstract way - which also makes it easier for other languages (like Java) to pick it up.


TS is great but lack of pattern matching cripples the language somewhat


The tc39 pattern matching proposal seems to have some renewed energy with new champions (including from the typescript team) and syntax proposals:

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

Also there has been movement in the user land space with libraries like ts-pattern that make use of new features in typescript 4.x to provide basically the full pattern matching experience:

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


It helps that is has good smart-casting, you can do with an if where other languages need pattern matching

    const x: { t: 'A', foo: string } | { t: 'B', bar: string } = /*...*/
    return x.t === 'A' ? x.foo : x.bar


It is cool syntax trick, but it won’t make compiler tell you when you missed a case


What does the typewritable type do/allow?


I'll give some brief context first for why it could be useful, then explain what DeepWritable does. AWS Amplify JS has a library called DataStore that takes GraphQL schema and generates ActiveRecord-like model classes (https://github.com/dabit3/amplify-datastore-example/blob/mas...). Then you can create, query, update, and delete these models in your web app client. The generated classes have are immutable and have read-only fields by default. This means none of the model objects will change under you before they've actually been persisted, which is a good guarantee to have IMO.

To make updates, you create a new model object with the same ID and whatever changes you need, and persist it back. DataStore uses an immer-like mechanism where each model object has a copyOf method, to which you pass a function that takes a mutable version of the model and makes whatever mutations. Then the copyOf method returns a copy of the model object that has all the new field values, leaving the original object the same. The DeepWritable type expresses the "mutable version" of the model by making all fields writable, so for example the TypeScript compiler won't yell at you for writing to fields on the mutable version of the model. It also does this recursively for any fields whose type is an object that isn't a primitive.


It helps that Erich Gamma and Anders Hejlsberg are on the team, with deep experience in developer tooling.


I was looking at dotty/scala 3 earlier in the year and I was quite surprised to see familiar friends from Typescript in the form of literal types and union types.

val scala: "Scala" = "Scala"

const typescript: "Typescript" = "Typescript"

"Scala" | "Typescript"


This creates two types with quotes in their type names, and then creates a union type of the two? Then the compiler needs to determine if the token's context requires a type or a value, and disambiguate accordingly?

In the example above, the created type "Scala" is distinct from a type Scala, correct?

I did some quick poking around for Scala literal types, but I found stuff like https://github.com/jeremyrsmith/literal-types , which don't look like the example above.


Literal types quite simply means that values are types. For example in TypeScript the literal type "Foo" is a special string type, where the only valid value is the literal "Foo" – assigning "Bar" to a variable with the type "Foo" is a compile time type error. Literal types can be combined into union types like any other type.


Sounds like more Scala sophistry. I thought they were supposed to be aiming for pragmatism with Scala 3.


I use it in Typescript often. Very useful.

type Verb = 'GET' | 'POST' | 'PUT' | 'DELETE';

export type TerminusType = 'ICAO' | 'IATA' | 'LOCODE' | 'ADDRESS' | 'LATLNG';

It's much better than just assuming it's a string and hoping you don't make a typo.

If the value is supplied from user input then it ensures that you write checks or switch cases.


> I use it in Typescript often. Very useful.

> type Verb = 'GET' | 'POST' | 'PUT' | 'DELETE';

> export type TerminusType = 'ICAO' | 'IATA' | 'LOCODE' | 'ADDRESS' | 'LATLNG';

> It's much better than just assuming it's a string and hoping you don't make a typo.

> If the value is supplied from user input then it ensures that you write checks or switch cases.

Conceptually, how is this different to an enum?


It can be used as an enum and might be more convenient in places where you don't need to bother converting between string data outside the system and enum data types.

It also plays nicely with union types, so that you can very quickly define as hoc enums directly in your function declarations, which might be useful if there's only one or two usages of a particular enum, or a section of code where the allowable values very quickly changes.

You basically get a lot of expressiveness without boilerplate, which can be pretty convenient.


Conceptually, it's very much like an enum. Except that you can't enumerate them at runtime because there is no runtime representation. This can be very frustrating.

There's a hack, it looks like:

    export const VERBS = ['GET', 'POST', 'PUT', 'DELETE'] as const;
    type VerbTuple = typeof VERBS;
    export type Verb = VerbTuple[number];
A real enum would be better, but would violate the "typescript is just javascript" rule.


An Enum in ts is weird. It's not just a type, it's something compiles to something in the runtime.

So to use the value you have to import the Enum and use that.

const handlers = { '/': { Verbs.GET: () => .... Verbs.POST: () => ...

Whereas:

const handlers = { '/': { GET: () => .... PAWST: //nope! must be one of GET | POST | PUT | DELETE

Basically it's a lot less work and you don't need to import the enum and maintain it.


This is just making clear and easy to use something that people did using some type level magic (see shapeless Witness).

Something that people used through ugly hacks becoming part of the language natively in a easy to use and understand (and faster to compile) way sounds very much like pragmatism to me.

Here is the source code to Witness (and other singleton stuff in shapeless) https://github.com/milessabin/shapeless/blob/main/core/src/m...


I can think of plenty cases where I would want to limit the input space of a function to a known set of literals.

Just as an example; the `document.getContext` [0] api takes a string argument to select the context canvas.

Without literal types, you cannot enforce this to a set of valid values. Literal types gives you the power to tell the compiler, which values are valid.

Since there are other programming languages that support literal types, it's not just Scala sophistry. :-P

[0] https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasE...


that's exactly how it works in TS. You can call TS sophistry then. Most people I talk to put it on the pragmatic side


They're describing typescript...


No. It's a Singleton type, which means that instead of just being a String, it has to be exactly the String "Scala".

i.e.

val x: "scala" = "scala" // would compile

val y: "scala" = "java" // would produce a compile error

Union types are an orthogonal concept, and work as you'd expect:

val z: String | Int = "foo"


Interesting, and I presume it works for singletons of any type, correct?

    val x: 1 = 1
    val y: 2.718281828 = 2.718281828
That's kind of cute. I presume its main purpose is for overrides in order to get something similar to template specialization. C++ templates can be specialized by types and by values, but Scala method overrides are only by type, and literal types allow you to present a value as a type, so if the value can be statically proven to match, then the override is called?


In TypeScript perhaps the most important use case for literal types is the ability to to create discriminated unions, or algebraic datatypes (ADTs). Many modern languages have a separate feature for this (such as enums in Rust), but in TypeScript they can be constructed using literal and union types. Here's an example:

    interface ProductOffer { 
        kind: 'productOffer'
        eans: Ean[]
        discountPercentage: number
    }

    interface GroupOffer {
        kind: 'groupOffer'
        groups: GroupId[]
        discountPercentage: number
    }

    type Offer = ProductOffer | GroupOffer

Also check the TypeScript handbook for more information: https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...


I think it is important to emphasize that there is actually an important difference.

Union types don't allow to model GADTs, so they are less powerful than what some other languages offer. But on the other side, they allow for much easier composition. E.g.:

type Citrus = "Orange" | "Lemon" type Prunus = "Plum" | "Apricot"

Now in typescript you can just do:

type Fruit = Citrus | Prunus

But in many other languages it's not so easy and you have to jump through hoops and e.g. either redefine the types and/or refactor the code (if you can even do that - if the code is from 3rd party libraries you are out of luck).


It's not really about specialization (types in Scala tend to be parametric most of the time) so much as just being able to have values in types, so that you can have types like "ListOfLength[1]" or "MapContainingKeys["Foo" :: "Bar" :: Nil]" without having to do some horrendous encoding of those things as types.


Literal types ("Scala" instead of String) existed before typescript to my knowledge. But union types are such a great thing. I'm very happy to see Scala catching up with typescript, as it has proven to work out really well.


I played with it a bit because I really like TS's union, literal types and smart-casting, unfortunately it doesn't seem quite there yet and I found it only usable for T | null types.

Syntax is all there so maybe it'll get better in 3.1+


Would be interesting if you can provide some examples. I'm happy to check them and turn them into tickets / feature requests for you.


I encountered them quite soon, e.g. something like this [1]:

   val result: Int | "INTERNAL_SERVER_ERROR" = 1
   val m: Int = result match {
      case "INTERNAL_SERVER_ERROR" => throw new Exception("Uh oh")
      case t => t
    }
It does work with a ClassTag and t: T, but then it thinks the match is not exhaustive. Compare that to Typescript [2].

In this specific language feature TS is kinda the gold standard. For example they can automatically narrow the type based on fields existing or not and their type, which can be used to provide a pretty decent implementation of sum types [3]

[1] https://scastie.scala-lang.org/oMdkLBIiRsqUkXv6qbP61g

[2] https://www.typescriptlang.org/play?#code/MYewdgzgLgBATgUwgV...

[3] https://www.typescriptlang.org/docs/handbook/unions-and-inte...




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

Search: