Rewriting TypeScript in Rust?
totaltypescript.com
totaltypescript.com
But then I find some third party code which seems to use the kitchen sink approach to typing, AKA the "look at how smart we are" approach, and it's a nightmare to understand. A type system is supposed to make code easier to work with, by the time you're spending more time understanding the types than the actual code there's something wrong.
In these cases I wish Typescript was more limited and I would gladly give up a lot of the new stuff to enforce simple types across the ecosystem.
That said, I don't really have an opinion on who is at fault here - coders for writing overly elaborate types, or Typescript for allowing them.
Backwards compatibility is to blame. Typescript was designed to support a number of patterns which was already used in JavaScript.
For example events are registered in the DOM like `element.addEventListener("click", (ev) => ...)`. The type of the callback will depend on the string passed as the first argument, so the type system need to support literal strings as type discriminators which is a weird feature not necessary in most other languages. If the API had been designed with static typing in mind from the beginning, it would have been designed differently and wouldn't need such a complex type system.
Now take mapped types, template literal types, key remapping... Those features aren't necessary to express legacy DOM APIs, this is just for the sake of having a complex typing system. And that's not to add a new feature that unlocks some type magic that wasn't possible before. No, those are just shorthands for things that you could already express in the existing typing system.
For example the 'options' parameter is a common pattern where an object with a set of properties are provided to override a some default behavior. So you need to define a Foo type with a set of properties, and then you need to define a type which is like Foo except all properties are optional.
Some frameworks implement their own flavor of inheritance e.g. through mixins. The type of an object with mixins is not just the sum of all properties, you might also have one property overriding a property of a different type in base mixin, so you are adding and removing properties. You need a really powerful type system to be able to represent this.
Magic string are use all over JavaScript for different purposes. For example createElement('P') could just be new PHtmlElement() ...
In other cases magic strings could be replaced with enums.
I was curious how other languages did discriminators in a way that makes Typescript's feel alien. Just having different functions doesn't really tackle that.
type Shape = Square Int | Circle Int
and then pattern matching: print (Square size) = ..
print (Circle radius) = ...
In Typescript this would be something like: type Shape = { type: 'square', size: number } | { type: 'circle', radius: number }
And string matching for type narrowing: function print(shape: Shape) {
if (shape.type === 'square) {
...
} else if (shape.type === 'square) {
...
}
}
The TypeScript approach is comparatively weird and verbose but also pretty clever since it doesn't require any change to the core language.It also have some pitfalls, e.g. you can write:
print({type: 'square', size: 10});
But you cant write: const x = {type: 'square', size: 10};
print(x); <-- type error
You have to write: const x = {type: 'square' as const, size: 10};
print(x);
Things like this just complicates the type system and type inference. foo(get_some_myenum_value(), …)
{type:get_another(), …}
These are as unguessable as if myenum was a string.This may be true in a lot of cases, but certainly not all.
I’ve spent a ton of time on the types that validate that the input/output schema of my request handler function matches the defined schemas, and that was hell, but now any other of our developers will see delightfully red squiggles when they do something wrong. That’s a force multiplier and absolutely worth the effort.
Example the new satisfies and some upcomign "as const" features to generics I'm looking forward to
Also try exploring npm/telegraf classes (auto-documented) by looking at their type definitions. The whole bot api could have been written as simple repeated definitions, but someone chose the clever way of multi-level type mutations and derivatives.
Typescript will soon need secondary community-maintained type contracts that are human-readable and cross-checked with those in libraries. Something like @humane-types/*.
(I say this not as a dig against TypeScript or the way it's been designed; I just think the parent wants it to be something fundamentally different from what it is)
This is the primary argument against worrying about N+1 standards. We know it's bad, if we simply do nothing at all, we know it will get worse. We can and should at least try.
All those legacy features and existing pages are the back pressure that keeps the evolution of the web stable.
It does reach admirably far into checking the regular dynamic-language programming patterns like using string variables to index into structures. Which is where a lot of its complexity comes from, but I wouldn't call that stuff "deranged". JS is a dynamic programming language, and people write it like a dynamic programming language, and typescript decided to meet it where it was at, which is probably the only reason it's gotten so much traction in the first place
1. Use an enum
2. Use an intersection with an enum and literally anything else (except a number)
3. Use a private field (only works with classes)
4. Use a `declare const FOO: unique symbol` keyed object intersected with anything else (works with everything including numbers, lots of boilerplate to declare a new nominal type).
In particular iirc you can't use your nominal/branded type as a map key in a type-safe way.
enum A {}
type B = { a: number } & A;
function newB(i:number) {
return { a: i } as B;
}
let b:B = newB(5);
let m:Map<B, string> = new Map();
m.set(b, "test");
m.set({ a: 2 }, "test"); // errors declare const NOMINAL_BRAND: unique symbol;
enum UserIdType {}
type UserId = string & {[NOMINAL_BRAND]: UserIdType}
type UserMap<V> = { [key: UserId]: V };
But at some point in the past few years, this is now supported and things like this work as expected: const userId: UserId = "234" as UserId
const coolUsers: UserMap<boolean> = {}
coolUsers[userId] = true // works
coolUsers["isThisOkay"] = false // failsBut anyway, what I want from TS is
type UserId = unique string; // or something
const userId = "1234" as UserId
and no further boilerplate or trickery to get it to work. Would certainly be nice.But that won't be added because there is no native JavaScript equivalent.
So to differentiate behavior depending of which part of sum type you got you need some value in the "real world" like a tag that can enable you to do that. Because all the type information is gone at runtime.
Strongest violation of this rule I know of are enums that generate their own JS code, but they are their separate thing. Something like sum types would need to be present everywhere.
Honestly I kind of like it this way. I'm trying out Rust right now and I'm very often surprised how what I do in one spot depends on huge context of all the generic types that can create wildly different behavior (mostly huge variety compile type error messages in surprisingly varied and distant spots) depending on what types they infer.
TypeScript also supports some really advanced typing I've yet to see in other languages like template literal types (don't @ me, I mostly work with dynamic languages at my current job). I feel like even the TS docs haven't caught up to the full power of TS's full featureset
The other issue with the typesystem is the (soon to be obviated) need to compile to decent javascript and interact with it. That made sense before WASM existed and it will make a lot less sense once the component model is stabilized.
Always check ts-essentials for the ideal abstractions built with the wild new features.
Is it safe to say a non-zero portion of "the community" is headed the direction of:
write it in _____ + compile it to WASM + glue DOM stuff to it with some sort of JS bridge?
Just search github.com and you will find countless example repos for many languages, but understanding typescript first is probably a more productive use of most people's time.
If you work in web-tech, I consider avoiding JS and TS to be almost negligent to some extent in 2022.
Say I have `type Foo = 'bar' | 'baz'` and want to validate that an input string is within the set of Foo. Why can't I just enumerate the type to determine the valid values at runtime?
Occasionally I bump in the same things as you when I'd like to have some JS code generated on the basis of TS type information. I think there could be some preprocessing tools that could generate JS validators on the basis of TS code.
Now, trying Rust I strongly appreciate this separation. In Rust what your code DOES might depend on what you are going to do in the future with the result it returns. It's really shocking for me and makes finding errors really hard when the spot of the error messages jumps wildly across large chunk of your code as I change it.
I really prefer TS philosophy of "types are for you and your text editor and the actual code is for runtime".
Is there something inherent with TypeScript that is causing issues that you can point to?
IIRC the TypeScirpt leadership is of the position that the implementation is the specification, which is unfortunate, because it means that the bugs in the implementation are necessarily part of the specification as well.
- gc
- gccgo
- gollvm [0]
Maintaining a language specification is a lot more work than people realize, and a job that few have the skills to do well.
C/C++ spec does have some "implementation defined behavior", which is where you can't rely on what will happen unless you know what compiler you're targeting. They are explicitly listed out as such, and not that common.
Partial specification like the grammar, the type system, etc are more common (particularly because they're so useful for writing one implementation, let alone multiple!).
If you're talking about ruby/spec, a comprehensive test suite is really great, but is also not what most people think about when they think of a spec, though it also may count! You could even argue that such a thing is better than a traditional spec.
Having a spec is really valuable only if you intend to target different environments, that's a non-goal for TS it simply transpiles, rather than compiles, into another programming language.
It's also bound to follow each new JS feature and change.
Which is okay as long as you only ever have one implementation, but when you want to, say, reimplement Typescript in Rust then you have to duplicate all of the quirks that came before, even in cases where nobody has yet realized that the quirk exists. Failure to do so means that your implementation won't be compatible with the canonical implementation.
At the other end of the spectrum, the Go project maintains two independent compiler implementations to ensure that the spec is implementable and to see that a single implementation, with all of its quirks, doesn't end up defining the language. That puts a lot of confidence in the spec to guide additional implementations, but, of course, is a lot of added work.
Tradeoffs, as always.
Without the specs, there is no way to tell if tac is behaving as developers intended it
You want 'evidence' tsc is not working as it should, by writting a duplicated less specifying specification that would sometimes conflict with the typescript one.
It make me remember the people that wanted to specify everything and generate implementation from it: that called a programming language.
Specification is meant for compiler author. Writing one of them is easier than otht
In fact, while poorly named, what we call testing is actually specification. A test suite specifies for software authors what a program is intended to do and how it is intended to be used. Of course, as a nice side bonus, because the specification is delivered in code the documentation can be validated for truthfulness against an implementation by machine.
It's not doing what I expect. Oh? Change the code
How is that different from now?
1.It's not doing what I expect.
2.Is it intentional behavior or a bug?
3. Let's ask on github/reddit etc
With Special:
1. It is not doing what I expect
2. Is this a spec issue or code issue?
3. Let's check spec.
4a. "Working as specified". Shit. Hope spec improves in future versions
4b. It is not working as spec says. Let's file a bug on tsc.
And that is the difference.
Also, when done well a spec is a great way to learn & understand the language too. http://golang.org/ref/spec is so much more usable than Typescript's docs.
A spec resolves such discrepancies. Indeed, the spec may be where the problem is, but once the spec is corrected then implementations know what they must do. Absent of a spec, who knows?
1) a reference implementation,
2) written in natural language, or
3) written as formal semantics
#1 is the same as the current situation. With #2, you might get a bit closer but natural language has many ambiguities and idiosyncrasies that can render it useless in various situations.
You can try to define formal semantics, but that is not only hard to create, it's also hard to translate into an implementation, adding significant overhead in both parts of the process. And there can still be bugs, and there can still be edge cases with undefined behavior.
This kind of certainty is very at odds with TypeScript's design philosophy and the way it (necessarily) works. Because it's overlaid on JavaScript, and JavaScript is wildly dynamic (Proxies, prototype modification, etc), almost nothing is known with absolute certainty. To deal with that, TypeScript makes a lot of "optimistic" assumptions, but holds them lightly (i.e. its assumptions may cause type checks to pass when they shouldn't, but it's not going to lean on them for anything that could really blow up if they're wrong)
IMO it strikes a very good balance given the constraints - not criticizing the designers at all - but TypeScript has to be treated differently than other statically typed languages might be, because nearly everything it knows/says is just a "probably"
What makes you think that? For example for C language it has been significant effort to translate the specification to something analyzable by tools
I am a little surprised nobody at Microsoft has started working on an official native compiler (or maybe they have, and we just don't know about it yet). It does feel like they've hit a ceiling
I think WASM is still not a good fit for a typechecking server.
I also don't see why WASM wouldn't be a good fit for that
Again- any references for this claim? Genuinely curious if this is part of TypeScript's known history, or if it's just speculation
It compiles to native code via C++.
It compiles to native code via C++.
> The most challenging thing is the absence of the specification. I had to infer everything just from test cases.
> The second thing is the velocity of tsc. It's way too fast, and I was unsure if I could follow it up. So I decided to use semi-automated translation and selected Golang.
> But it was too boring, and more importantly, my programming ability has improved enough to follow tsc only by myself.
I think the reasons they switched are pretty weak, but justified at the same time. Kdy1 looks like more of a Rust person (their GitHub has more active Rust repos than Go), so this should’ve been the choice from the beginning. Going with the comfortable choice over the “pragmatic” one is almost always the best option if you’re the only contributor (or plan to be for a while).
I wonder if there’s an opportunity for collaboration here!
https://rome.tools/blog/2021/09/21/rome-will-be-rewritten-in...
But if you're also re-interpreting the code, which seems to be Donny's approach, you can get bogged down with the discrepancies. The more the two diverge, the harder it is to debug discrepancies and keep track of updates in the upstream code.
I'm also porting a static analysis tool from an interpreted language to Rust, but it's a fairly straightforward port and I've been able to stick to schedule pretty well.
Sounds more like they should purposely break compatibility form time to time to prevent Donny from catching up.
Besides, such malicious action can easily be counterproductive.
People can just stop using tsc and switch completely to Rust version. Then, it would effectively make any development on tsc useless.
Perhaps a better way MS could contribute is to define a formal specification for the type checker, which would make efforts like this a lot easier.
But these aren’t mutually exclusive.
Achieving 100% compatibility on public code bases and switching is a great strategy at this point.
All of that for what, slightly faster compilations? TS server is already fast enough for me to develop giant ts codebases in codespaces on 2 vCPUS with 4 GB of ram.
"... in VSCode, the red lines and warnings take a long time to update on large projects.
... absence of [a] specification" for TypeScript's behaviour. Donny's had to infer TypeScript's behaviour mostly test cases alone.
The pure size of TypeScript is daunting. It's had ten years to iterate, grow and add features. "
---
Therefore for practical reasons, I stick with plain JavaScript (on Node.js etc.) and use a small assertions library
https://www.npmjs.com/package/ok6
Assertions are the ultimate "types", they can assert any property you can express in your programming language. The key is to be able to make them succinct so they don't obscure your main code.Yes they don't give you compile-time type-checking, which would be useful indeed. But as hinted at the excerpts above, in practice compile-time type-checking can slow you down.
In most case all I need is a way to prove to myself that "if this function runs a set of tests-cases, then the assertions in it are truthful. And, if anybody calls this function with wrong types of arguments my assertions will tell me there is a problem.
Often enough of a good thing is enough. You don't need the most sophisticated language and type-system to create robust programs quickly.
This is backwards. Having your type system be unplanned and ad-hoc is what makes it slow and complex (thus TypeScript, which had to retrofit a type system onto JavaScript), and making types be arbitrary runtime code is the epitome of that. There are better points on the expressivity/constraint tradeoff, but you'll only ever reach them by designing them into your programming language up-front; you can't retrofit consistency.
But the benefit of assertions is they can express arbitrary requirements, not only about the type of each argument and result but also about intra-arguments relationships between arguments, and also between arguments and the result.
As an example I could say:
ok (argA < argB);
...
ok (result > argA - argB);
It's a tradeoff: More expressivity, simplicity and speed of development vs. earlier detection of type-errors.If I write an assertion `x is a number`, then fine, statically analyze that, that works today.
If I write a hard-to-compute assertion like `x is Prime` (or `x is a counterexample to the Riemann Hypothesis`), then I don't care if the compiler can statically figure it out. It can simply require all callers to test that before calling this function, they assert `x is Prime` directly -- no analysis required. This way my function can be sure it is only called with prime numbers, but nobody has to write any logic in a typechecker that can figure out whether that's true for arbitrary inputs.
However, even in weaker type systems you can get a similar effect by just creating a PrimeNumber type, and for functions that require a prime number, only accept PrimeNumber. Then any number you want to pass to those functions will have to go through PrimeNumber's constructor.
Preferably during running of your unit-tests.
In practice much of the software we use today has runtime errors. How the code handles those varies. Static typing does not prevent runtime errors does it?
Which means you need more unit test coverage, have more tests to maintain, and your code structure becomes more brittle.
> Static typing does not prevent runtime errors does it?
Often it does; and if a validation rule is too complex or externally-dependent to enforce at compile time, static typing at least lets you push checking to the start of an operation, rather than in the middle of your computation.
My point was more that static type-checking can be seen equivalent to assertions, except it checks those assertions at compile time and typically lacks some expressive power compared to assertions, in most practical programming languages today.
Static type-checking is more a feature of the compiler than of the language. Consider type-inference: You can have a language where types are not explicitly declared most of the time. Yet they can be checked at compile-time, assuming you have a compiler that is able to perform that amazing feat.
How is this in Rust?
Maybe the main point of four average Rust users publishing a crate each is so that burntsushi can publish one great one.
You're missing the point, these are ideas for further work.
If you meant parallel processes within the tooling, I suspect web workers wouldn't be the solution (they're part of the Web API). Node does expose a worker thread API, which they might already use in the typescript repo (I'm not familiar with any of their code).
Probably not for all portions of the compiler. But I think with some reinforcement learning, it ( ... might ...) be possible to automatically generate a robust Typescript parser in Zig.
Honestly, I'm still not sure if people could breathe without things like air.