Node.js adds experimental support for TypeScript
github.com
github.com
Consider for example: `foo < bar & baz > ( x )`. In TypeScript 1.5 this parsed as (foo<bar) & (baz > (x)) because bar&baz wasn’t a valid type expression yet. When the type intersection operator was added, the parse changed to foo<(bar & baz)>(x) which desugared to foo(x). I realise I’m going back in time here but it’s a nice simple example.
If you want to continue to use new TypeScript features you are going to need to keep compiling to JS, or else keep your node version up to date. For people who like to stick on node LTS releases this may be an unacceptable compromise.
Updating node is much more fraught than updating TypeScript. For example, it may break any native code modules. That’s why users are directed to use the LTS and not the most recent release, so that there’s enough time for libraries to add support for the new version.
On the other hand, I usually adopt a new TypeScript version as soon as it comes out.
For production use, I'd still put my TS files through a build pipeline as normal
("Boring" in this context is a compliment, by the way)
EDIT: Though reading other comments, it seems like you can update the typescript stripper independent of node? That makes this moot anyway
With a couple exceptions (like enums), you can strip the types out of TypeScript and end up with valid JS. What you could do is stabilize the grammar, and release new versions of TypeScript using the same grammar. Maybe you need a flag to use LTS grammar in your tsconfig.json file.
Apart from a clueless engineer attempts them, it's been working out fine to pretend they don't exist.
To me beyond v4.4 or so, when it started being possible to create crazy recursive dependent types (the syntax was there since ~4.1 - it's just that the compiler complained), there weren't a lot of groundbreaking new features being added, so unless an external library requires a specific TS version to parse its type declarations, it doesn't change much.
Some edge-cases involving those have bugfixes and ergonomy improvents I've run into on 5.x.
That being said I regret every use of `infer` in application code.
Unlike Python typing it's not only type erasure: enums, namespaces, decorators, access modifiers, helper functions and so on need to be transformed into their JavaScript equivalent.
> There is already a precedent for something that Node.js support, that can be upgraded seperately, its NPM. Node bundles a version of npm that can upgraded separately, we could do the same with our TypeScript transpiler.
> We could create a package that we bundle but that can also be downloaded from NPM, keep a stable version in core, but if TypeScript releases new features that we don't support or breaking changes, or users want to use the new shiny experimental feature, they can upgrade it separately. This ensures that users are not locked, but also provides support for a TypeScript version for the whole 3 years of the lifetime of Node.js release.
But it would be a step backward to need to globally upgrade TypeScript (as you do with npm), since some older projects will not be compatible with newer versions of TypeScript.
Ask me how I know. ;)
It won't, but in such a scenario, typescript would only be a type checker, wich is a entirely different endeavor than running typescript.
I think Rust made the really smart move of putting :: before any type parameters for functions. Go made the good move of using square brackets for type parameters.
We really need five sets: grouping, arrays/indexing, records, type parameters, and compound statements. Curly braces {} are also overloaded in JS for records and compound statements, leading to x => {} and x => ({}) meaning different things.
Square brackets wouldn't work for parametric functions because f[T](x) already means get the element at index T and call it.
That's an interesting topic. Do you happen to know if UTF-8 contains more "pair" characters? In Latex we call these delimiteres, but that's just my limited experience coming in from math side. I tend to agree that it would be helpful to have more kind of nesting/pairing/grouping/delimiting characters. The problem is my imagination is limited to what I know from the ASCII world, and so it goes... no idea what new sets would look like.
Which brings us to the philosophical question about paired characters: If we were to pick paired characters from a key set not available on everyone's keyboard, why must the paired characters even be used in real languages? Is that not actually actively detrimental when we end up needing the characters for real? Is this not why we even have escape characters to begin with?
Plus, must they be a part of anyone's real keyboard to begin with? What makes 「」any more valid than ¿? Could we not have saved ourselves a lot of mental strain if we solved it earlier on with a full set of truly uncommon characters?
I can imagine an alternate history, where some programming language in the late 70's made their editors with simple shortcuts (such as Ctrl+A for "array block") to input barely-if-ever-used yet low code character such as † or ‡, which would never be used outside of a string. And nowadays, with modern IDE's, we wouldn't even see those characters half the time. It would be syntax sugar, with blocks types stated in gutters and data types represented with colors or text.
https://www.reddit.com/r/rust/comments/5penft/comment/dcsq64...
UTF-8, UTF-16, UTF-32 all have the same characters.
In most languages, (grouping and compound statements) cannot syntactically appear in the same place as (indexing, records, type parameters, function calls). So we are immediately down to four.
Rust takes the approach that you use :: before type parameters, so they are easily distinguished from comparison operators at a syntactic level.
Go takes the approach that [] is just fine for type parameters—which seems pretty reasonable to me. In Go, there’s nothing that can be both indexed *and* take a type parameter.
True, but TypeScript has a rule against type-dependent emit - it’s not allowed. Code must always do the same thing regardless of what the types are. And in any case JavaScript does allow indexing on functions, since functions are just objects.
(And they could even converted to unicode by the code formatter)
There was no perfect solution available, so a tradeoff was necessary. You can disagree with this particular tradeoff, but had they gone another way some people would disagree with that as well. To be a mistake there would have had to have been an option available that was clearly better at the time.
Anyway, the idea that TS 5 should be backwards compatible with TS 1 is probably a bad one. Personally, I think packages with wide usage break backwards compatibility far too easily -- it puts everyone who uses it on an upgrade treadmill, so it should be done very judiciously. But even I wouldn't argue that TS 1 should have been its final form.
You could argue that it was a C++ mistake. It makes parsing harder, but otherwise seems to work as expected, so I don't consider it a mistake, but you could at least argue that way.
But regardless if it was a mistake in C++, it's now a complete standard, used in C++, Java, C#, and other languages to denote type parameters.
I would argue that it would have been a mistake to break that standard. What would you have used, and in what way would that have been enough better to compensate for the increased difficulty in understanding TypeScript generics for users of almost every other popular language?
It's still just (early in the process) Stage 1, but the majority of Typescript's type syntax, for the purposes of type stripping (not type checking), is attempting to be somewhat standardized: https://github.com/tc39/proposal-type-annotations
When TypeScript added const declarations, they added it as `as const` so a type stripping could have still worked depending on how loosely it is implemented.
I think there is a world where type stripping exists (which the TS team has been in favor of) and the TS team might consider how it affects type stripping in future language design. For example, the `satisfies` keyword could have also been added by piggy-backing on the `as` keyword, like:
const foo = { bar: 1 } as subtype of Foo
(I think not using `as` is a better fit semantically but this could be a trade-off to make for better type stripping backwards compatibility) function foo<T>() {
return bar(
null as unknown as T extends boolean
? true /* ): */
: (T extends string
? "string"
: false
)
)
}
function bar(value: any): void {}
Any solution I can come up with suffers from at least one of these issues:- "ignore rest of line" will either fail or lead to incorrect results - "find matching parenthesis" would have to parse comments inside types (probably doable, but could break with future TS additions) - "try finding end of non-JS code" will inevitably trip up in some situations, and can get very expensive
I'd love a rough outline or links/pointers, if you can find the time!
[0] TS Playground link: https://www.typescriptlang.org/play/?#code/AQ4MwVwOwYwFwJYHs...
In JS, because it is a fun example, "end of statement" is defined in large part by Automatic Semicolon Insertion (ASI), whether or not semicolons even exist in the source input. (Even if you use semicolons regularly in JS, JS will still insert its own semicolons. Semicolons don't protect you from ASI.) ASI is also a useful example because it is an ancient example of a language design intentionally trying to be resilient. Some older JS parsers even would ignore bad statements and continue on the next statement based on ASI determined statement break. We generally like our JS to be much more strict than that today, but early JS was originally built to be a resilient language in some interesting ways.
One place to dive into that directly (in the middle of a deeper context of JS parser theory): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
The example snippet I added is designed to violate the rules I could come up with. I'd specifically like to know: what are better rules to solve this specific case?
I don't know anything about parsers besides what I learned from that one semester worth of introduction class I took in college but from what I understand of your question, I think the answer is you can't simply because we can't look into the future.
1. Automatic semicolon insertion would next want to kick in at the } token, so that's the obvious end of the statement. If you've asked it to ignore from `as` to the end of the statement (as you've established with your "ignore to the end of the 'line'"), that's where it stops ignoring.
1A. Obviously in that case `bar(null` is not a valid statement after ignoring from `as` to the end of the statement.
2. The trick to your specific case, that you've stumbled into is that `as` is an expression modifier, not a statement modifier. The argument to a function is an expression, not a statement. That definitely complicates things because "end of the current expression" is often a lot more complicated than ASI (and people think ASI is complicated). Most parsers are going to have some sort of token state counter for nested parentheses (this is a fun implementation detail of different parsers because while recursion is easy enough in "context-free grammars" the details of tracking that recursion is generally not technically "context-free" at that point, so sometimes it is in the tokenizer, sometimes it is a context extension to the parser itself, sometimes it is using a stack implementation detail of the parser) and you are going to want to ignore to the next "," token that signals a new argument or the next ")" that signals the end of arguments, with respect to any () nesting.
2A. Because of how complicated expression parsing can get, that probably sets some resiliency bounds on your "ignorable grammar": it may require that internally it still follows most of the logic of your general expression language: balanced nested parentheses, no dangling commas, usual comment syntax, etc.
2B. You probably want to define those sorts of boundaries anyway. The easiest way is to say that ignorable extensions such as `as` must themselves parse as if it was a valid expression, even if the language cannot interpret its meaning. You can think of this as the meta-grammar where one option for an expression might be `<expression> ::= <expression> 'as' <expression>` with the second expression being parseable but ignorable after parsing to the language runtime and JIT. You can see that effectively in the syntax description for Python's original PEP 3107 syntax-only type hints standard [1], it's surprisingly that succinct there. (The possible proposed grammar in the Type Annotations proposal to TC39 is a lot more specific and a lot less succinct [2], for a number of reasons.)
Comments as in your example is typically stripped in the tokenization stage so would not affect parsing. The TpeScript type syntax has its own grammar, but it uses the same lexical syntax as regular JavaScript.
A “meta grammar” for type expressions could say skip until next comma or semicolon, and it could recognize parentheses and brackets as nesting and fully skip such blocks also.
The problem with the ‘satisfies’ keyword is a parser without support would not even know this is part of the type language. New ‘skippable’ syntax would have to be introduced as ‘as satisfies’ or similar, triggering the type-syntax parsing mode.
> For example, the `as` keyword for casts has existed for a long time, and type stripping could strip everything after the `as` keyword with a minimal grammar.
My question is: what would a grammar like this look like in this specific case?
TypeAssertion ::= Expression “as” TypeStuff
TypeStuff ::= TypeStuffItem+
TypeStuffItem ::= Block | any token except , ; ) } ]
Block ::= ParenBlock | CurlyBracketsBlock | SquareBracketsBlock | AngleBracketsBlock
ParenBlock ::= ( ParenBlockItem* )
ParenBlockItem ::= Block | any token except ( )
etc.Let’s say that typescript adds a new type operator “wobble T”. What does this desugar to?
x as wobble
T
Without knowing about the new wobble syntax this would be parsed as `x as wobble; T` and desugar to `x; T`With the new wobble syntax it would be parsed as `x as (wobble T);` according to JS semicolon insertion rules because the expression wobble is incomplete, and desugar to `x`
type T = keyof
{
a: null,
b: null
}
Here T is “a”|”b”, no automatic semicolon is inserted after `keyof`. While I don’t personally write code like this, I’m sure that someone does. It’s perfectly within the rules, after all.While it’s true that TS doesn’t have to follow JS rules for semicolon insertion in type expressions, it always has done, and probably always should do.
We find Typescript much easier to upgrade than Node. New Node versions change performance characteristics of the app at runtime, and sometimes regress complex features like async hooks or have memory leaks. We tend to have multi-week rollout plans for new Node versions with side-by-side deploys to check metrics.
Typescript on the other hand someone can upgrade in a single PR, and once you get the types to check, you’re done and you merge. We just got to the latest TS version last week.
It's interesting, though, that this approach in Python has led to several (4?) different popular type checkers, which AFAIK all use the same type hint syntax but apply different semantics. However for JavaScript, TypeScript seems to have become the one-and-only popular type checker.
In Python, I've even heard of people writing types in source code but never checking them, essentially using type hints as a more convenient syntax for comments. Support for ignoring types in Node.js would make that approach possible in JavaScript as well.
https://github.com/google/closure-compiler/issues/2731
I happen to know this because we have some old projects that depend on this and are working hard to get rid of the dependency.
I wish Google either updates it or just mark the whole thing deprecated -- the world has already moved on anyway. Relating this to Google's recent cost cutting, and seeing some other Google's open source projects more or less getting abandoned, I have to say that today's Google is definitely not the same company from two decades ago.
With normal Javascript and typescript, you can't minify property names, so `foo.bar.doSomethingVeryComplicated()` can only be turned into `a.bar.doSomethingVeryComplicated()`, not `a.b.c()`, like with Closure. This is because objects can be indexed by strings. Something like `foo.bar[function]()` is perfectly valid JS, where the value of `function` might come from the user.
A minifier can't guarantee that such expressions won't be used, so it cannot optimize property accesses. Because Closure was a type checker and a minifier at the same time, it could minify the properties declared as private, while leaving the public ones intact.
A minor thing but `function` is a keyword in JS so technically it's not a "perfectly valid JS".
Given TypeScript’s type system is unsound, neither could it even if it tried, right? I guess Flow could, but well, here we are.
But there are TSconfig options to ensure no use of any so with the right level of strictness it could happen.
I'm asking because there's no accepted definition of what an unsound type system is.
What I often see is that the word unsound is used to mean that a type system can accept types different to what has been declared, and in that case there's nothing unsound about ts since it won't allow you to do so.
Typescript is a bit more unsound than most because of the escape hatch `any` and because of the (intentional) disconnect between compiler and runtime environment. Even though "unsound" sounds like a bad thing to be, it's a big part of why Typescript is so successful.
For example if you have a Java expression of type MyClass, and it gets evaluated, then it must either throw (so that it doesn't produce any value) or produce a value of type MyClass: either an instance of MyClass, or of one of its subclasses, or null. It will never produce an instance of some other class, or an int, or anything else that isn't a valid value for the type MyClass.
In addition to helping human readers reason about the code, a sound type system is a big deal for a compiler: it makes it possible to compile the code AOT to fast native code, without inserting a bunch of runtime checks and dynamic dispatching to handle the fact that inevitably some of the types (but you don't know which) are wrong.
The compiler implications are what motivated the Dart language's developers to migrate from an unsound to a sound type system a few years ago: https://dart.dev/language/type-system#the-benefits-of-soundn... so that they could compile Flutter apps AOT. This didn't require anyone to make their code resemble what you'd do in a theorem prover — it just means that, for example, all casts are checked, so that they throw if the value doesn't turn out to have the type the cast wants to return.
TypeScript is unsound because when you have an expression with a type, that tells you nothing at all for sure about what the value of the expression can be — it might be of that type, or it might be anything else. It's still valuable because you can maintain a codebase where the types are mostly accurate, and that's enough to help a lot in reading and maintaining the code.
The purpose of typescript is usefully type as much javascript as possible, to do both this and have a sound type system it would require to change javascript.
A prime example is that if you index into an array of type `T[]`, JS semantics mean the value you get back could be undefined as well as a `T`. So to describe existing JS semantics in a sound type system, the type would have to be `T | undefined`, which would be a big pain. Alternatively you could make the type `T` and have that be sound, but only if you make the runtime semantics be that an out-of-bounds access throws instead of returning undefined.
The problem is that you can arbitrarily narrow types (and any can narrow to any type) eg: https://www.typescriptlang.org/play/?#code/DYUwLgBAzg9grgOwC...
If your type system has at least two types that aren't the same as each other, then adding "any" makes it unsound right there. The essence of "any" is that it lets you take a value of one type and pretend it's of any other type. Which is to say that "any" is basically the purified form of unsoundness.
Consider this example (https://www.typescriptlang.org/play/?ssl=10&ssc=1&pln=1&pc=1...):
function messUpTheArray(arr: Array<string | number>): void {
arr.push(3);
}
const strings: Array<string> = ['foo', 'bar'];
messUpTheArray(strings);
const s: string = strings[2];
console.log(s.toLowerCase())
Could you explain how this isn't the type system accepting types "different to what has been declared"? Kinda looks like TypeScript is happy to type check this, despite `s` being a `number` at runtime.In Haskell land, where the type system is considered sound you have `head` functions of type `List a -> a` that are unsound too, because the list might be empty.
However this problem as stated is slightly different and has to do with a failure of OOP/subtyping to actually intermingle with our expectations of covariance.
So to just use classic "animal metaphor" OOP, if you have an Animal class with Dog and Cat subclasses, and you create an IORef<Cat>, a cell that can contain a cat, you would like to provide that to an IORef<Animal> function because you want to think of the type as covariant: Cat is a subtype of Animal, F<Cat> should be a subtype of F<Animal>. The problem is that this function now has the blessing of the type system to store a Dog in the cell, which can be observed by the parts that still consider this an IORef<Cat>.
Put slightly differently, in OOP, the methods of IORef<Cat> all accept an implicit IORef<Cat> called `this`, if those methods are part of what define an IORef<x> then an IORef<x> is necessarily invariant, not covariant, in <x>. And then you can't assume subtyping. So to be sound a subtype system would presumably have to actually mark contra/covariance around everything, and TypeScript very intentionally documents that they don't do this and are just trying to make a "best effort" pass because JavaScript has 0 types, and crappy types are better than no types, and we can't wait for perfect types to replace the crappy types.
Haskell's `head` not is not an example of the type system being unsound (I stress this point because we've been talking about type system soundness, not something-else-soundness).
From the view of the type system, `head` is perfectly sound: if the list is empty, the resulting value is ⊥ ("bottom"). And ⊥ is an inhabitant of every type. Therefore, `head` returning ⊥ when given an empty list is perfectly fine. When you force ⊥ (i.e. use it any way whatsoever), an exception is thrown. See https://wiki.haskell.org/Bottom
This is very much not the same thing (or remotely analogous) to what we have in my TypeScript example. There, the code fails at runtime when I attempt to call `toLowerCase`, yes; what's worse is the slightly different scenario where we succeed in calling something we shouldn't:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
kill() {
console.log("Killing: " + this.name);
}
}
class Murderer extends Person { }
class Innocent extends Person { }
function populatePeopleFromDatabase(people: Array<Innocent | Murderer>): void {
// imagine this came from a real SQL query
people.push(new Innocent("Bob"));
}
function populateMurderersFromDatabase(people: Array<Murderer>): void {
// TODO(Aleck): come back and replace this with a query that only selects murderers.
// i wanted to get the rest of the code in place, and this type checks,
// so I'll punt on this for now and come back later when I wrap my head
// around the proper SQL.
// we're not actually using this anywhere just yet, so no biggie ¯\_(ツ)_/¯
populatePeopleFromDatabase(people);
}
// ... some time later, Bob comes along and implements the murderer execution logic:
const murderers: Array<Murderer> = [];
populateMurderersFromDatabase(murderers);
// Bob is about to have a really shitty day:
murderer.forEach((murderer) => murderer.kill());
It is not possible to write an analogous example in Haskell using `head`.Huh?
The cheeky answer would be that the definition here is the one the TypeScript documentation itself uses[1].
The useful answer is that there’s only one general definition that I’ve ever encountered: a type system is sound if no well-typed program encounters type errors during its execution. Importantly, that’s not a statement about the (static) type system in isolation: it’s tied to the language’s dynamic semantics.
The tricky part, of course, is defining “type error”. In theoretical contexts, it’s common to just not define any evaluation rules at all for outwardly ill-typed things (negating a list, say), thus the common phrasing that no well-typed program must get stuck (unable to evaluate further). In practical statically-typed languages, there are on occasion cases that are defined not to be type errors essentially by fiat, such as null pointer accesses in Java, or escape hatches, such as unsafeCoerce in practical implementations of Haskell.
Of course, ECMAScript just defines behaviour for everything (except violating invariants in proxy handlers, in which case, lol, good luck), so arguably every static type system for it is sound, even one that allows var foo: string = 42. Obviously that’s not a helpful point of view. I think it’s reasonable to say that whatever we count as erroneous situations must at the very least include all occurrences of ReferenceError and TypeError.
TypeScript prevents most of them, which is good enough for its linting use case, when the worst possible result is that a buggy program crashes. It would definitely not be good enough for Closure Compiler’s minification use case, when the worst possible result is that a correct program gets silently miscompiled (misminified?).
[1] https://www.typescriptlang.org/docs/handbook/type-compatibil...
If you read the Flow codebase and its Git history, you can see that it's not for lack of trying, either — every couple of years there's an ambitious new engineer with a new plan for how to make it happen. But it's a real tough migration problem — it only works if they can provide a credible, appealing migration path to the other engineers across Facebook/Meta's giant JS codebase. Starting from a language like JS with all the dynamic tricks people use there, that's a tough job.
(And naturally it'd be even harder if they were trying to get any wider community to migrate, outside their own employer.)
I wonder how widely used that option is. As I said in that other comment, it feels to me like the sort of thing that would produce errors all over the place, and would therefore be a real pain to migrate to. (It'd be just fine if the language semantics were that out-of-bounds array access throws, but that's not the semantics JS has.) I don't have a real empirical sense of that, though.
I don't think it ever actually did this. It renamed all properties (you could use the index syntax to avoid this) and just used a global mapping to ensure that every source property name was consistently renamed (no matter what type it was on). I don't think type information was ever actually used in minification.
So if you had two independent types that had a `getName` function the compiler would always give them the same minified name even though in theory their names could be different because they were fully independent types. The mapping was always bijective. This is suboptimal because short names like `a` could only be used for a single source name, leading to higher entropy names overall. Additionally names from the JS runtime were globally excluded from renaming. So any `.length` property would never be renamed in case it was `[].length`.
Both choices are reasonable ones to make. Flow has some really cool stuff, and works great for a lot of people.
There’s no denying, though, that there’s TS has done something right (even if you personally dislike it)
const arr = ["abcd"];
const str = arr[1];
const num = str.length; // this throws
console.log(num);
For me, typescript is a pretty good balance.Compare these two programs.
const arr = ["abcd"];
const str = arr[1];
const num = str.length; // this throws at runtime
const arr = [new Date];
const dt = arr[1];
const num = dt.length; // fails to type check main = putStrLn (["a", "b", "c"]!!4)The result of the an OOB access of an array is specified to be `undefined`. The throw only happens later when the value is treated as the wrong type.
I don't consider a runtime error to be a failure of the type system for OOB array access. But in javascript, it's explicitly allowed by specification. It's a failure of any type system that fails to account for this specified behavior in the language.
This is like arguing that a null exception is fine because it's allowed by the language. If you get `undefined` when you expect another type, most future interaction are guaranteed to have JS throw because of the JS equivalent of a null pointer exception. They are technically different because a dynamic language runtime can prevent a total crash, but the effect on your web app is going to be essentially the same.
[1,2,3][4].toFixed(2)
> It's a failure of any type system that fails to account for this specified behavior in the language.Haskell has the ability to handle the error.
How do you recommend a compiler to detect out-of-bounds at compile time? It can certainly do this for our trivial example, but that example will also be immediately evident the first time you run the code too, so it's probably not worth the effort. What about the infinite number of more subtle variants?
I wouldn't make the recommendation that they do at all. Full soundness is not my thing. But... if Flow wanted to do it, it would have to change the type of indexing into `(Element[])[number]` with a read from `Element` to `Element | undefined`.
I went looking for where on their website they claim to be sound. There's definitely some misleading wording here: https://flow.org/en/docs/lang/types-and-expressions/#toc-sou... but if you read the whole section, it ends up also acknowledging that it's not entirely sound.
This isn't a type error unless your type system is also encoding lengths, but most type systems aren't going to do that and leave it to the runtime (I suspect the halting problem makes a general solution impossible).
main = putStrLn (["a", "b", "c"]!!4)Soundness is good as long as the type-checking benefit is worth the cost of the constraints in the language. If the poster child for soundness isn't able to account for this very simple and common scenario, then nothing will actually be able to deliever full soundness.
It's just a question of how far down the spectrum you're willing to go. Pure js is too unsound for my taste. Haskell is too constrained for my taste. You might come to a different conclusion, but for me, typescript is a good balance.
Put another way, SML is all the best parts of TS, but with more soundness and none of the worst parts of TS and non of the many TS edge cases baked into the language because they keep squashing symptoms of unsoundness or adding weird JS edge cases that you shouldn't be doing anyway.
Or stated more abstractly: if an expression has type T, and at runtime the expression evaluates to a value v, then v has type T.
The language can still have runtime errors, like if you try to access an array out of bounds. The key is that such operations have to give an error — like by throwing, so that the expression doesn't evaluate to any value at all — rather than returning a value that doesn't fit the type.
Both TypeScript and Flow are unsound, because an expression with type "string" can always turn out to evaluate to null or a number or an object or anything else. Flow had the ambition to be sound, which is honorable but they never accomplished it. TypeScript announced up front that they didn't care about soundness: https://www.typescriptlang.org/docs/handbook/type-compatibil...
Soundness is valuable because it makes it possible to look at the types and reason about the program using them. An unsound type-checker like TypeScript or Flow can still be very useful to human readers if most of the types in a codebase are accurate, but you always have to keep that asterisk in the back of your head.
One very concrete consequence of soundness that it makes it possible to compile the code to fast native code. That's what motivated Dart a few years ago to migrate from an unsound type system to a sound one: https://dart.dev/language/type-system so that it could AOT-compile Flutter apps for speed.
One of the biggest revolutions in JS JITS was the inline cache (IC). It allows fast lookup and specialized functions (which would be too expensive otherwise). These in turn allow all the optimizations of the higher-tier JITs.
The biggest problem of Flow and TS is encouraging you to make slow code. The second you add a generic to your function, you are undoubtedly agreeing that it is going to be accepting more than 4 types. This means your function is megamorphic. No more IC. No more optimization (even worse, if it takes some time to hit those 5+ types, you get the dreaded deoptimization). In theory, they could detect that your function has 80 possible variations and create specialized, monomorphic functions for each one, but that's way too much code to send over the wire. That kind of specialization MUST be done in the JIT.
If you bake the types into the language via a `"use type"` directive, this give a LOT of potential. First, you can add an actually sound type system. Second, like `"use strict"` eliminated a lot of the really bad parts of JS, you can eliminate unwanted type coercion and prevent the really dynamic things that prevent optimization. Because the JIT can use these types, it can eliminate the need for IC altogether in typed functions. It can still detect the most-used type variants of a function and make specialized versions then use the types to directly link those call sites to the fast version for even more optimization.
I use TS because of its ubiquity, but I think there's the possibility for a future where a system a little more like Flow gets baked into the language.
JS uses a JIT while Ocaml is AOT which is generally an advantage for Ocaml.
Ocaml only compiles once while JS compiles every time it runs. This means that JS is a lot more selective about its compilation, but the hot code could be every bit as fast as Ocaml. On the flip side, Ocaml is fast because it compiles method-at-a-time and a SML compiler like MLton which does a slow whole-program pass can generate significantly faster code (despite being a part-time hobby project for a few academics).
The big difference is money. Ocaml has some funding, but nothing compared to JS. It's hard to believe, but handling strings in JS is probably faster than what most devs could do themselves in C/C++. It's not because JS is inherently faster. It's because those bits are native and have had countless man-years poured into making them fast. That said, even the JIT itself is top-tier and raw integer code is only 20-50% slower than C (excluding any SIMD optimizations).
I think the upper limit for a typed JS could be about as fast and maybe a little faster than Ocaml on the JIT and maybe even a little faster with a more restrictive subset compiling to WASM.
Have you looked into ReScript? It is basically a sound type system + JavaScript-like syntax. It inherits the type system from OCaml. You might like it. They recently released version 11.
There was: Anders Hejlsberg and Lars Bak: TypeScript, JavaScript, and Dart https://www.youtube.com/watch?v=5AqbCQuK0gM (2013).
Summary: https://g.co/gemini/share/a60c3897bae1 / https://archive.is/qJ1wA
One big annoyance with Flow I had is like you said: unpolished tooling. Another was frequent breaking changes (I don't hold it against them too much, it was 0.x software after all)
Also because features diverged, you had to maintain type defs for multiple versions of flow for for multiple library versions. And then at one point, they also decided to convert internal errors to any types instead of displaying the error. That was the last straw for me, especially since I maintained a few flow type defs. I spent _so_ much of _my_ time just on type def maintenance for open source libraries already, with the any decay I like flying blind too. So I just switched to TS with its interior type system: it was good enough and others maintained library typedefs for me. But now the type systems are much more closely aligned (unless flow drifted), so switching to TS paid off in the end.
This is my main approach. Type hints are wonderful for keeping code legible/sane without going into full static type enforcement which can become cumbersome for rapid development.
But if I were putting in type hints like this, I'd still definitely want them to be statically checked. Its better to have no types at all than wrong types.
I agree - but the type systems of both Python and TypeScript are unsound, so all type hints can potentially be wrong. That's one reason why I still mostly use untyped Python - I don't think it's worth the effort of writing type annotations if they're just going to sit there and tell lies.
Or maybe the unsoundness is just a theoretical issue - are incorrect type hints much of a problem in practice?
Fwiw I’ve been working with TypeScript for 8+ years now and I’m pretty sure wrong type hints has never been a problem. TS is a God-send for working with a codebase.
Do you have an anecdote (just one!) of a case where TypeScript's lack of type system soundness bit you on a real application? Or an anecdote you can link to from someone else?
The only time an issue ever came up for me was in dealing with arrays
let foo: number[] = [0, 1, 2]
// typed as number but it’s really undefined
let bar = foo[3]
But once you’re aware of the caveat it’s something you can deal with, and it certainly doesn’t negate the many massive benefits that TS confers over vanilla JS.Sure. The usual Java-style variance nonsense is probably the most common source, but I see you're not bothered by that, so the next worst thing is likely object spreading. Here's an anonymized version of something that cropped up in code review earlier this week:
const incomingValue: { name: string, updatedAt: number } = { name: "foo", updatedAt: 0 }
const intermediateValueWithPoorlyChosenSignature: { name: string } = incomingValue
const outgoingValue: { name: string, updatedAt: string } = { updatedAt: new Date().toISOString() , ...intermediateValueWithPoorlyChosenSignature }And yes, TypeScript types are "at least these properties" and not "exactly these properties". That is by design and is frankly one reason why I like TypeScript over Java/C#/Kotlin.
I'd be very interested to know what you'd do to change the type system here to catch this. Are you proposing that types be exact bounds rather than lower bounds on what an object contains?
'{foo :: Int | bar} is a record with a known property 'foo' and some unspecified properties 'bar'. You cannot pass a `{foo :: Int, bar :: Int}` into a function that expects `{foo :: Int}`.
A function that accepts any record with a field foo, changes foo, keeping other properties intact has the type
{foo :: Int | bar} -> {foo :: Int | bar}The issue isn't that it got overridden, it's that it got overridden with a value of the wrong type. An intermediate type signature with `updatedAt` as a key will produce a type error regardless of the type of the corresponding value.
> I'd be very interested to know what you'd do to change the type system here to catch this.
Like the other commenter said, extensible records. Ideally extensible row types, with records, unions, heterogeneous lists, and so on as interpretations, but that seems very unlikely.
A language has a sound type system if every well-typed program behaves as defined by the language's semantics during execution.
Go is structurally typed, and yet it is sound: code that successfully type checks is guaranteed to abide the semantics of the language.
TypeScript is unsound because code that type checks does not necessarily abide the semantics of the language:
function messUpTheArray(arr: Array<string | number>): void {
arr.push(3);
}
const strings: Array<string> = ['foo', 'bar'];
messUpTheArray(strings);
const s: string = strings[2];
console.log(s.toLowerCase())
`strings` is declared as a `Array<string>`, but TypeScript is happy to insert a `number` into it. This is a contradiction, and an example of unsoundness.`s` is declared as `string`, but TypeScript is happy to assign a `number` to it. This is a contradiction, and an example of unsoundness.
This code eventually fails at runtime when we try to call `s.toLowerCase()`, as `number` has no such function.
What we're seeing here is that TypeScript will readily accept programs which violate its own rules. Any language that does this, whether nominally typed or structurally typed, is unsound.
def mersenne(p: int): return 2**p - 1
Or, given there's no need for the type hints to be checker-friendly, do you make them more human-friendly, e.g: def mersenne(p: 'prime number'): return 2**p - 1```
from typing import NewType
# Create a new type for some_prime SomePrime = NewType('SomePrime', int)
def process_prime(value: SomePrime) -> int: return value ```
However, this isn’t nearly as common as simply using a more descriptive argument name like “prime_number : int”
One of the big advantages to type hinting in Python is that it feeds the IDE a lot of information to increase auto-complete functionality, so you want to avoid things like p:”prime number”
Yeah, until this discussion I thought the main benefit of type hints was earlier detection of bugs via static checking. Now though, I'm getting the impression that the bigger benefit is enabling IDE features such as autocomplete.
That helps me understand better why I haven't found type hints as useful as others seem to - I don't use an IDE. My use of Python is limited to small scripts which I write in a simple text editor.
I think this should be part of the language spec.
E: I thought it was JSDoc proposal. Ignore the comment.
They just can't have browsers doing the actual type checking because there isn't a specification for how to do that, and writing one would be extremely complicated, and I'm not sure what the point would be anyway.
Note that there's IDEs that'll use type hints to improve autocomplete and the like too, so even when not checking types it can make sense to add them in some places.
The biggest pain point of using JSDoc at least for me was the import syntax, this has changed since Typescript 5.5, and it's now not an issue anymore.
The moment you need to declare or extend a type you’re done, you have to do so in a separate .ts file. It would be possible to do so and import it in JSDoc, but as mentioned before it’s a huge PITA on top of the PITA that writing types can already be (e.g. function/callbacks/generics)
I use this approach professionally in teams with many developers, and it works better for us than native TS. Honestly give it a try, I was skeptical at first.
For me the advantages of just having JS files and not worrying about more complex source-maps, build files, etc definitely makes it worth it.
In a world where ES modules are natively supported everywhere it’s a joy to have a project “just work” with zero build steps. It’s not worth it in a large project where you’re already using five other plugins in your build script anyway but for small projects it’s a breath of fresh air.
https://www.typescriptlang.org/docs/handbook/2/functions.htm...
It's fine on toy projects, and somewhat I would say, for 99% of users that don't even know what a mapped or intersection type is.
JSDoc is indeed fine on toy project, or in fact any project (even prod-ready ones) that doesn't warrant the trouble of adding NPM packages and transpilation steps.
Although they are rare, those type of small, feature-complete codebases do exists.
Node.JS isn't the only JS runtime. You'll still have to compile TS to JS for browsers until all the browsers can run TS directly. Although some bundlers already do that by using a non-official compiler, like SWC (the one Node's trying out for this feature).
> In Python, I've even heard of people writing types in source code but never checking them, essentially using type hints as a more convenient syntax for comments.
It's not just comments. It's also, like the name "type hint" suggests, a hint for your IDE to display better autocomplete options.
Ah yes, autocomplete is another benefit of machine-readable type hints. OTOH there's an argument that another IDE feature, informational pop-ups, would be better if they paid more attention to comments and less to type hints:
https://discuss.python.org/t/a-more-useful-and-less-divisive...
I personally would very much prefer if NPM modules that have their original code in TS and are currently transpiling would stop shipping dist/.cjs so I unambiguously know where to put my debugger/console.log statements. And it would probably be very tempting to NPM contributors to not have to bother with a build step anymore.
But won't this start a ripple effect through NPM where everyone will start to assume very quickly 'everyone accepts TS files' - it only takes one of your dependencies for this effect to ripple through? It seems to me that nodejs can't move this outside an opt-in-experimental-flag without the whole community implicitly expecting all consumers to accept TS files before you know it. And if they do, it will be just months before Firefox and Safari will be force to accept it too, so all JS compilers will have to discard TS type annotations
Which I would personally be happy with - we're building transcompiling steps into NPM modules that convert the ts code into js and d.ts just to support some hypothetical JS user even though we're using TS on the including side. But if node accepts .ts files we could just remove those transpiling steps without ever noticing it... so what's stopping NPM publishers from publishing js/d.ts files without noticing they broke anything?
For a new thing I was writing from scratch, yeah, I might just ship typescript and not bother transpiling.
[edit: Apparently not. TS is only for top-level things, not libraries in node_modules according to the sibling comment from satanacchio who I believe is the author of the PR that added TS support and a member of the Node.js Technical Steering Committee]
Besides, it's already going through one transpilation step to go from TS to ESM, so adding a second one for CJS really isn't that much hassle.
I think if node.js had made require() work with ESM, I could probably drop CJS. But since that's probably never going to happen, I'm just going to continue shipping both versions for old projects and not worry about it.
Nobody is arguing for that. Once you ship ESM, you can continue shipping ESM.
In Node 22 you can even require() ES modules (with an experimental flag, at the moment)
> Nobody is arguing for that. Once you ship ESM, you can continue shipping ESM.
I'm not sure I follow you there. I did continue shipping ESM.
> In Node 22 you can even require() ES modules (with an experimental flag, at the moment)
Oh, I didn't know about that, cool! Once it becomes un-flagged I might consider dropping CJS.
Why is making downstream have to switch to `await import()` that big of a deal?
You can use async/await in CJS just fine. Sure, sometimes you may need to resort to some ugly async IIFE wrappers because CJS doesn't support top-level await like ESM does, but is that really such a big deal?
Sure, it's a breaking change, but that's what semver major bumps are for.
I just think that if projects want to stay in CJS they should learn how to use async/await. I clearly don't understand why CJS libraries feel a need synchronous require() for everything. (Though to be fair, I've also never intentionally written anything directly in CJS. I learned enough in the AMD days to avoid CJS like a plague.)
> You can use async/await in CJS just fine. Sure, sometimes you may need to resort to some ugly async IIFE wrappers because CJS doesn't support top-level await like ESM does, but is that really such a big deal?
It might seem like a small amount of work, but for a library one must to multiply that small amount of work by the number of users who will have to repeat it. It can be a quite large amount in aggregate. And, for what benefit? So I can drop one line from my CI config? It just seems like a huge waste of everyone's time.
Also, as a library user, I would (and occasionally do) get annoyed by seemingly unnecessary work foisted on my by a library author. It makes me consider whether or not I want to actually depend on that library, and sometimes the answer is no.
This is probably where we have the biggest difference in our calculations. I know there's a lot of pain in legacy CJS systems, but from my view (which is maybe more "browser-oriented", which is maybe a bit more Deno/Bun-influences, which comes from a "Typescript-first" mentality going way back to 0.x) it is more legacy "giant balls of mud" maintained by a sparse few developers. I don't see this multiplicand as very big on the scale of library user count. Most CJS for years and years has been transpiled from Typescript or Rollup; most CJS only exists to be eaten by Webpack or other bundler, many of which today rewrite CJS to ESM anyway. From what I see a lot of CJS seems either transpiled out of habit (for some notion of supporting Node < 10 that doesn't make sense with current security support) or by accident (by a misconfigured tsconfig.json, for example, and then often looping back through a transpiler again back to ESM). The way we cut through the Gordian knot of we're all doing too much transpilation to/from CJS is to start eliminating automated transpilation to CJS in the first place. Which is why I find it useful every time to ask people what they are really trying to do when transpiling to .cjs today.
Of course, if your multiplicand is lines-of-code impacted, because I agree there are some great big huge piles of mud in CJS that are likely stuck that way for lack of developers/maintainers and lack of time/budget/money, then worrying about the minority of users still intentionally using CJS is worth caring about, and my sympathies in that situation.
You, of course, know your library's users better than me and maybe you do have a lot of CJS users that I just wouldn't consider in my calculations. I'm not going to stop you from transpiling to CJS if you find that necessary for your library. That's your judgment call. I just wanted to also make sure to ask the questions of "do you really need to?" and "how many users do you actually think it will impact?" out loud. Thanks a lot for the conversation on it, and I'm still going to be a radical banging the "CJS Must Die" drum, but I understand pragmatism and maintenance needs, especially those of legacy applications, and mostly just want to make sure the conversation is an active one and a lot less of passively transpiling stuff that doesn't really need it.
I'm a game developer. I make web games. We run our games with a simple nginx server that simply serves the wasm. We have some JavaScript libraries we use. They have to be raw dog .js.
I don't even know what your "ejs" or "cjs" acronyms mean.
We use the discord JavaScript SDK. Discord only ships it as a node module or as .ts.
It's a pain in our ass to update because we don't know what those tools your talking about are and we don't want to know. Just give me the damn .js
I'd rather kill ESM. And it's not fully supported in Node. It doesn't work in the REPL.
That said with "type": "module" in my own package.json files, I've so far never had a problem importing legacy CJS npm packages into ESM, other than the Types are more likely to be wrong (because packages that only publish CJS are more likely to also not publish their own types) or at least inaccurate for the current import approach (returning only "synthetic default" instead of individual exports, for example). That's a bunch of papercuts having to attempt multiple imports until you understand what shape Node is giving you of that CJS import, but after those papercuts I feel like interop is generally smooth sailing in today's Node.
might have to look into it again but there was an issue where some react/jsx/fragment bit had to be ported to esm first.
(My own .tsx based view library doesn't yet have official SSR support, but I do all my testing in ESM in the built-in Node test runner `node --test` so I don't see any complications in doing .tsx on the "server-side", because that is how I'm testing everything already, I just haven't entirely figured out my "hydration" or "islands" or "stamps" approach so I don't officially support SSR yet. It's on the roadmap and I've made small bits of progress towards it, just need to solve it and haven't had the time/priority.)
Essentially what it does is allow you to upload your typescript code without a build step so when other devs install it they can see the source code of the module in it's original typescript instead of transpiled JavaScript.
Apparently you can only view the uncompiled source code in deno since it natively supports typescript.
My bad
That would be a huge win. Right now in Python, great tools like pydantic exist because Python can introspect said types, and generate checks out of them.
This mean you can define simple types, and get:
- type checking - run time data check - api generation - api document generation
Out of a single, standard notation.
Right now in JS, things like zod have to do:
const mySchema = z.string();
Which is basically reinventing what typescript is already doing.The parent commenter was talking about a way for nodejs to provide, via an API, the content of type annotations on fields/functions/variables like in python.
However, in python the type annotations are a property of the object at run time, whereas they are completely stripped before execution for typescript.
So I'm not sure how it would work except by changing the typescript philosophy of "not changing runtime execution"
However, in TS other type information is erased at compile time. So if you write
type Foo = "a" | "b";
the runtime code will see that just as a plain string.For those unfamiliar:
`z.string()` effectively converts `mySchema` into a functional schema capable of parsing and validation.
For example:
`mySchema.parse("some data")` returns successfully.
`mySchema.parse(321)` throws an exception.
I've used it in places where you need runtime validation and in process verification - it works pretty well for that and you can extract the types from it via :
const A = z.string(); type A = z.infer<typeof A>; // string
Meaning if you define your types in zod first, and infer their types from that you get compile and runtime type checking.
---
It a bit of an overkill for nimble and fast code bases though - but works wonders for situations where in process proofing needs to be done, and in all honesty it isn't that big of a task to do this.
Well of course it offers more, or you wouldn't be installing a library.
The problem is that even when you're expressing normal Typescript types, you have to use entirely different syntax. It's good that you can usually avoid double-definition, but it's still a big barrier that shouldn't be necessary.
Having said that, TypeScript allows plugins, these are very rarely used as they augment the language by introducing other features that are transformed into the resulting JavaScript files. One plugin that relates to your suggestion of run time types is called Typia, it permits you to use your TypeScript type signatures at runtime with guards like `assert<MyType>(myValue)` where it intercepts the function call to construct an exhaustive if statement in the transpiled JavaScript checking the nature of the passed variable.
So while I don't see it being a part of the language in the next four to six years, there are at least libraries out there already that allow you to do it today.
We need a type system that is actually sound and TS is intentionally unsound. We need a type system that doesn't allow bad coding practices like TS does. We need a type system that enforces program design that allows programs to be fast. We need a Hindley Milner type system.
If you want a module to be typed, add a `"use type"`. This should disallow bad parts of the language like type coercion. It should disallow things that hurt performance like changing object shape/value type or making arrays of random collections of stuff. Incoming data from untyped modules would either coerce or throw errors if coercion can't be done at which point the compiler can deeply-optimize the typed code because it would have far stronger type guarantees and wouldn't have a risk of bailing out.
Does TS help you keep your functions monomorphic so they'll get optimized by the JIT? nope
Does TS keep your object shape from changing so it will get optimized by the JIT? it actively does the opposite giving TONS of tools that allow you to add, remove, modify, and otherwise mess up your objects and guarantee your code will never optimize beyond the basic bytecode (making it one or two orders of magnitude more slow than it could otherwise be).
TS doesn't do anything to prevent or even discourage these kinds of bad decisions. They "type soup" many projects fall into is another symptom of this. The big reason the types become such a mess is because the underlying design is a mess. Instead of telling programmers "fix your mess", TS just releases even more features so you can type the terrible code without fixing it.
Can you elaborate or point to some of the tools? So I know what tools I may need to avoid
If you have a monomorphic function (1 type), the IC is very fast. If you have a polymorphic function (2-4 types), the IC function gets quite a bit slower. They call 5+ types megamorphic and it basically foregoes IC altogether and also disables most optimizations.
TS knows how many variants exist for a specific function and even knows how many of those variants are used. It should warn you when your functions are megamorphic, but that would instantly kill 90% of their type features because those features are actively BAD in JS.
Let's illustrate this.
interface Foo {
bar: string | string[]
baz?: number
blah?: boolean
}
Looks reasonably typical, but when we use it: function useFoo(foo: Foo) { .... }
useFoo({bar: "abc", baz: 123, blah: true}) //monomorphic
useFoo({bar: "abc", baz: 123}) //now a slower polymorphic
useFoo({bar: "abc"})
useFoo({bar: ["b"], baz: 123})
useFoo({bar: ["b"], baz: 123, blah: true}) //we just fell off the performance cliff
As you can see, getting bad performance is shockingly easy and if these calls were across five different files, they look similar enough that you'd have a hard time realizing things were slow.Union/intersection aren't directly evil. Unions of a single type (eg, a union of strings) is actually great as it offers more specificity while not increasing function complexity. Even if they are a union of different primitive types, that is sometimes necessary and the cost you are paying is visible (though most JS devs are oblivious to the cost).
Optionals are somewhat more evil because they somewhat hide the price you are paying.
[key:string] is potentially evil. If you are using it as a kind of `any`, then it is probably evil, but if you are using it to indicate a map of strings to a type, then it's perfectly fine.
keyof is great for narrowing the possible until you start passing those keys around the type system.
Template unions are also great for pumping out a giant string enum (though there is a definite people issue of making sure you're only allowing what you want to allow), but if they get passed around the type system for use, they are probably evil.
Interface merging is evil. It allows your interface to spread across multiple places making it hard to follow and even harder to decide if it will make your code slow.
Overloads are evil. They pretend you have two different functions, but then just union everything together.
Conditional types are evil. They only exist for creating even more complex types and those types are basically guaranteed to be both impossible to fully understand and allow very slow code.
Mapped types are evil. As with conditional types, they exist to make complex an incomprehensible types that allow slow code.
Generics are the mother of all that is evil in TS. When you use a generic, you are allowing basically anything to be inserted which means your type is instantly megamorphic. If a piece of code uses generics, you should simply assume it is as slow as possible.
As an aside, overloads were a missed opportunity. In theory, TS could speed everything up by dynamically generating all those different function variants at compile time. In practice, the widespread use of generic everything means your 5mb of code would instantly bloat into 5gb of code. Overloads would be a great syntax to specify that you care enough about the performance of that specific function that you want to make multiple versions and link to the right one at compile time. Libraries like React that make most of their user-facing functions megamorphic could probably see a decent performance boost from this in projects that used TS (they already try to do this manually by using the megamorphic function to dispatch to a bunch of monomorphic functions).
type A = { value: number; }
function test(a: A) { a.value = 3; }
function main() {
const a: Readonly<A> = { value: 1 };
// a.value = 2; <= this errors out
test(a); // this doesn't error out
console.log(a); // shows 3
}Normally typescript does not just allow implicit removal of a container type.
Like, you can't pass Array<A> to a function that just takes A.
If only projects like Bun/Deno/Node added runtime support for ReScript instead of TypeScript, collectively as the web-tooling industry, we'd be in a better place. But you can't win against the MS's marketing budget.
Also in hindsight, ReScript diverged away from OCaml, but the ReScript development team could have gone further by creating a runtime for ReScript. Then again I don't blame them - they are polishing the dev experience of ReScript and React.
This is the decade of writing shiny new runtimes - I hope somebody writes a ReScript runtime. Imageine ReScript, Core, rescript-webapi, typechecker, re-analyze, plus a bundler minifier etc baked into the runtime like Bun. Sounds like an interesting value proposition. Fingers crossed.
it blows my mind weekly how ergonomic and flexible typescript’s type system is. it allows me to write great apis for my team mates.
is it possible for the type checker to end up in an infinite loop or for a junior developer to abuse “as”? absolutely, but it doesn’t really matter in practice.
i wouldn’t want to run typescript in rockets or submarines tho!
TS types are IGNORED by the JIT to generate SLOW code.
All the features that make Typescript more ergonomic for devs also allow it to generate slower JS code. AssemblyScript tries to be TS for WASM and it doesn't support huge swaths of TS because they output unusably slow garbage.
I also suspect that more than a few Ocaml ergonomic issues are due to nominal typing. StandardML's structural typing and inference give an experience very similar to TS, but without the major soundness issues (though it must be noted that ANY generics in JS will be slow unless the compiler is creating multiple function variants).
it's mainly the lack of ad-hoc polymorphism that makes ocaml feel a bit clunky to me at times. but structural typing sure would be nice.
i used to avoid typescript because of similar soundness issues. but in the context of web dev this weird type system that evolved from adding types to javascript turned out to be so nice to use. it's bonkers because on paper it shouldn't be this nice haha.
As I wrote elsewhere in this thread, TS makes it incredibly easy to unintentionally make megamorphic functions that don’t have any inline cache and don’t get optimized at all. You think you’re writing efficient, DRY code, but it’s really just dog slow because you’ve neutered the JIT.
Do you mean modular implicits?[1] The original paper was published in 2014. There's an internship report from 2023 that summarizes the design and implementation issues:
I believe modular implicits and modular typeclasses are similar and aim to solve the same issues, but they are not quite the same.
Meanwhile, I can’t configure node to not require extensions on import, nor have tsc configured to automatically add .js extensions to its compiled output, without adding on a bundler… although native TypeScript support would remedy this nit quite a bit, I can’t imagine the user experience (or performance) to match Bun’s when it reaches stable.
foo could mean foo/index.js, foo.js at the minimum. So you have 2x the lookups. Oh no, wait we also potentially have mjs, cjs, jsx, ts and tsx.
So 12 times the stat checking for each import.
Only in the worst case. If it's foo.js there's only one lookup.
> Oh no, wait we also potentially have mjs, cjs, jsx, ts and tsx. So 12 times the stat checking for each import.
Again, you're only taking into account the worst case.
The Developer Experience of unbundled ESM is great. Of course you do need to do things like always use file extensions. But those aren't hard changes to make, worth it for the better Developer Experience, and if it can help us start to wean off of mega-bundler tools as required production compile time steps.
It seems to be the default now:
$echo 'console.log("test")' > t.ts
$ tsc t.ts
$ ls
t.js t.ts
$ node t.js
testWell except for Deno...
However, the node:crypto module still doesn't work 100%. So, I can't use it yet.
I just tried to sign some data with an RSA key, and the results differed in Node and Bun.
Jokes apart, Zig is moving forward a lot which is why it's not 1.0 yet, but it doesn't mean you can't write safe and performant applications right now.
Zig is also a rather simple and straightforward language (like C) and has powerful compile-time code generation (like C macros, but without the awful preprocessor).
If you use frameworks written for node memory usage is very high and performance is meh.
If you use frameworks written for bun they smoke anything on node.
I'd definitely move over, just to get rid of the whole TypeScript / cjs / esm crap, but:
1. frontend support is poor (next.js / solid.js - I can't run anything fully on bun)
3. I still need to rewrite my backend app from a node.js framework to a bun one
4. for backend development the javascript ecosystem is losing the crown: if I wanted something safe I'd just write it in Rust (TS allows any random developer to write crap with any in it and it validates), if I'm doing something AI related I'd probably need python anyway and fastapi is not half bad
DX is great!
We use vite react ts, yoga, and prisma.
I guess we all just wanted java with JIT, more feature rich type system and gradual typing. Also for all the shortcomings of npm ecosystem, it is a lot less daunting and more fun to be using libraries in this ecosystem.
And surprisingly even though rust is on a different end of the language spectrum but yet it offers a similar feel.
Edit: JIT was not the right terminology to use. I lazily wrote JIT. Apologies. What I meant to convey was the difference in startup times and run time between running something in JVM and V8. Java feels heavy but in javascript ecosystem it feels so nimble.
Compilation times are also an absolute non-issue.
You don't compile for development. You do it for production (in the rare circumstances that you need it).
Also even though I also found java too verbose, I kept believing that we need it to be so to write good software. I still enjoy java but it doesn't compare to the ergonomics of typescript for me. And nimbleness of the experience according to me plays a decent role.
Currently for me, either I really care about performance and I default to rust for those applications or I need solutions where the product will evolve quickly over time and I need great DX over performance and I default to typescript for those.
Java definitely has a role to play but its role in my work has certainly diminished.
TypeScript is a direct superset of JavaScript. Any valid JS is a valid TypeScript.
If Kotlin is what TS to JS, then so is Groovy, Scala, Clojure and other JVM languages.
Kotlin on JVM is a direct superset of Java on JVM. Any valid Java is also valid Kotlin at the bytecode layer.
> If Kotlin is what TS to JS, then so is Groovy, Scala, Clojure and other JVM languages.
Correct
As an example, when my org needs to contact folks about potential mass shooter events, our SLA is 90 seconds. If we did it in cloud with java or .net, it'd be too slow to spin up. If we did it on prem, we'd be charged insane amounts just for the ability to instantly respond to low frequency black swan events, or it'd be too slow. This is a real story of how a Java dev team transitioned to using node for scale in the first place.
Of course CPU and RAM per request when compared to Node.js are not even close as Node is easily slower by a factor of 2-10.
One thing I miss that java has is runtime reflection of types though. Typescript's ecosystem has a million different ways to get around that and they're all a bit ugly imo.
Java has JIT. How is TypeSript type system feature-richer than the Java one?
It is how TS is able to "type" a completely untyped language.
Just the support for union types is something that not even Haskell or Ocaml have.
Conceptually no, almost every useful union type can be easily converted to a sum type. In my opinion the difference is in the ergonomics and in the implicit structural subtyping.
For example a common union type is number|string, and the beatiful part is that to use a value of such a type you do not need to do any matching or mapping you can just use the value as it does not have a runtime wrapper, for example (x:string|number)=>JSON.stringify(x) works perfectly fine.
Also you can have a function that takes as input a Array<string>|number|null and returns a string|number without having to declare different contructors for the input number type and the output number type
I believe that you can essentially implement this behaviour by generating enough typeclasses in Haskell, but regardless of the feasibility it, likely, would not be a good idea.
An example of something in between union types an Hindley–Milner sum types are Ocaml's polymorphic variants types https://ocaml.org/manual/5.2/types.html#sss:typexpr-polyvar that are (I believe) more advanced than TS unions but also a lot less ergonomic to use.
And TS has much more eg intersection types you could have a function with type
(x:number)=>string & (x:string)=>number
meaning that it is both a function that maps number to strings and strings to numbers (again you can do this with typeclasses but it is a worse experience)typescript also has very good support for value types for example there is the string type but also the "hello" type which is the type of only the string "hello"
All in all if someone told me that they implemented typescript in haskell typeclasses I would not call bullshit on them, but I would not believe that anyone would actually use it for anything
So that's like my beloved Perl then.
class MyNewClass extends OldClass {};
(of course it's not just a new name, it's also a new class, but it's also still a OldClass, and you are out of luck if OldClass is final or sealed)Java also has interfaces, of course. And optional properties (using Optional) and strict null checks, when you want that, you can use it.
very limited, for instance you must declare the type of a public method
> alias
as you point out it's not
> Java also has interfaces, of course
but you have to implement them explicitly
> strict null checks, when you want that, you can use it
if we start accepting static analysis tools then C has null checks as well I guess
so what's the difference except the name?
> if we start accepting static analysis tools
I'm not talking about static analysis. In today's Java you can write code that does not accept nulls, if you want to.
https://www.typescriptlang.org/docs/handbook/2/template-lite... alone puts TS over anything that Java has.
Virtual Threads alone challenge this assumption.
Syntax bloat is not a feature.
Some examples of TypeScript power:
- SQL database in TypeScript types: https://github.com/codemix/ts-sql
- Statically typed raw SQL queries: https://github.com/andywer/squid?tab=readme-ov-file#tag-func...
- (Someone fill in your TS hackery for me)
- BF: https://github.com/susisu/typefuck
- Assembly: https://github.com/judehunter/ts-asm
Utility types, like Partial<T>, are basically impossible to represent in Java except with almost-duplicated classes.
Exactly.
> Partial<T>
Looking at that it's just what a default POJO (with nullable properties) already is, so I'd see no need to represent that in Java.
Looks cool though and I like Typescript; my issue with it is that it needs transpiling to run. If it was a first-class citizen in an environment I would use it for my pet projects.
I think you missed the point. Partial<T> is an example of a "mapped type", see the handbook for more explanation: https://www.typescriptlang.org/docs/handbook/2/mapped-types....
https://gist.github.com/svieira/9f8beeafb7bf4aa55d40c638532f...
Java was literally the thing that made the term "JIT" popular, so I really don't know what you were going for here.
Also I just can't see how Typescript is in any way "closer" to Java - it's incredibly different IMHO. The only thing they have in common is probably the "Javascript" misnomer and the fact both support imperative programming, but that's it.
AKA dynamic typing. Unless it's 100% static, it's dynamic
Elixir's new type system does much better here, as it determines whether a function actually guards for the right type at runtime ("strong arrows") and propagates the guarantees, or lack thereof, accordingly.
God I wish they’d just integrate something lightweight like npm into JDK.
It is beyond me why you have to install third-party heavy weight tool just to manage dependencies.
I also want compiler/type checker niceties like exhaustive pattern matching.
In Typescript you have far more freedom, and all the benefits of strong types.
With TS you can prototype with JS and only after you know what you are looking for you can start to add types to find bugs and edge cases and want to get nice code completions for your stuff.
Oh god no. What an abomination.
Greater than 95% of the incompetence in JavaScript comes from two camps. The first of those are people who absolutely cannot program at all. The second of those are Java developers who were taught Java in school and it’s all they can do, so everything must look like Java.
The result of both tribes is pretending to do something they cannot do on their own. When you’re a pretender vanity becomes excessively important because everything is superficial, so you get layers of shit you don’t need that they cannot live without. Any attempts slice off the unnecessary bullshit always results in hyper emotional distress because people feel threatened when exposed. That right there is why I will never write JavaScript for employment ever again.
Maybe this means I don't always have to install esbuild to strip types - very excited how this will make writing scripts in TypeScript that much easier to use. I lately have been prefering Python for one off scripts, but I do think personally TypeScript > Python wrt types. And larger scripts really benefit from types especially when looking at them again after a few months.
Node is copying many of these features to varying degrees of success. But Deno is evolving, too.
In other words, the tooling could be better, but the fundamentals of URL imports are sound, IMO.
Furthermore, I think that it should be made so that the "hashed:" scheme that I had invented (in the Scorpion protocol/file-format specification document, although this scheme can be used independently of that) can also be usable.
And, popular hosts with well-known URL structures automatically templating also I would disagree, although it might do to allow any expressions in place of the string literals and then add functions for abbreviations of some of those URLs, if that would help (although I still think it is unnecessary).
Kidding aside: You should really take an hour and check out the manual and std lib (https://jsr.io/@std). I was surprised how far Deno has come. A lot of pretty useful stuff you would otherwise need tons of NPM modules for.
The JS ecosystem sure struggles with naming things.
I didn't intend to typo-squat. Actually, my package is older than ts-node and was just a pun because it is an API for TeamSpeak written in TypeScript.
Basically it boils down to swc being faster than esbuild, see their benchmarks:
You cannot run tsx from a non-project cwd if you’re using tsconfig/paths.
And personally I find its maintainers relatively unpleasant to message with. Leaves “you’re plebs” aftertaste most of the times.
> Node.js standard library requires additional hassle before using
I read the hassle as having to setup Node runtime in advance, but zx requires npm to install so I'm not sure.
i'm building a framework with minimal dependencies at https://www.plainweb.dev so i'm super excited about everything that node builds in (sqlite, typescript).
but there is a difference between supporting the bare minimum and actually making it nice to use day to day.
Bun came out swinging with strong Node.JS compatibility promises. I have simply replaced node with bun for most of my own work without much effort. The mental effort required is to use `bun` instead of `node` in command line for most of the trivial things.
Out of curiosity, what do you see as next steps, and what possible futures do you see for typescript in the node- and overall JS-ecosystem?
Not transpiling would be great to reduce toolchain complexity and eliminate the need for sourcemaps just to understand exceptions and debug.
Typescript seemed like a good option but was just another bolt on, I am not sure what value you gain by using Typescript over Golang, you have nice defined types which is great but it does not solve other issues with the language that are resolved in golang (also solved in deno).
One large benefit of using node over golang is the speed of prototyping something which I think having to use type script largely negates, so I can not really decide if this is a good step forwards or is making node loose some qualities that made it a good choice in other ways.
In the company I worked at we were fairly small and did not have huge applications running on node, so it made that journey easier
I've never used Django. Express seems a lot nicer.
jQuery was so popular because writing anymore than a few lines of vanilla JavaScript was an *awful* experience due to all differences in browsers.
When things eventually standardized-ish and jQuery became unnecessary, other libraries/ecosystems popped up (e.g. React/JSX) to make writing webapps easier because writing anymore than a few lines of vanilla JavaScript was still an *awful* experience.
When webapps grew in size and scope, other "transpiled" languages popped up (e.g. TypeScript) because writing anymore than a few lines of vanilla JavaScript is *still an awful* experience.
We're stuck with JavaScript due to past decisions, but let's not pretend it's actually a good tool. If it were we wouldn't need 50,000 tools/frameworks/transpiled languages to hide how terrible it is.
That's definitely a potential issue - JavaScript is the fundamental standard, not JQuery and not TypeScript. Certainly there are situations where maximum forward-compatibility is important (learning resources are a good example) and for those, vanilla JavaScript is the best choice.
All of the old resources that relied on JQuery are now hopelessly outdated, whereas the contemporary ones that used plain JavaScript are as valid now as when they were written. I'm sure the same will be true of TypeScript vs JavaScript when the next big thing comes along.
True, but on the other hand, almost any JS code snippet written 20 years ago has better and more elegant alternatives today. APIs evolve all the time.
I wonder bun and deno support for typescript played a big role here :)
How many days have you spent on webpack config? Or the package.json type property? Or yarn/pnpm/etc particulars?
I have spent too many.
Bun is quite nice.
*Or vite, or whatever equivalent.
I’m also not sure the features will eventually “tickle down”. I’m not sure NPM wants to adopt the advantages PNPM gives you as an example, and it’s probably a good thing too considering the basis of NPM is just a really solid system to build on top of which it wouldn’t be if it was very opinionated. One of the big issues Node has today is that it was very opinionated with CommonJS, which made sense at the time, but is a ginormous pain in the butt in the modern world. Though the blame is obviously not with Node alone.
node has a built in test runner now
I'm even willing to forgive all the mess that CJS vs. ESM is if they manage to pull this off.
I hope this sees widespread adoption/usage, which might finally cause some movement to integrate TS into ecmascript after all. Some dynamically-typed language fanatics (which are, in my opinion, completely detached from the reality that static types are what the vast majority of devs want) still have an iron grip on TC39, this might be the start of their end. And good riddance.
But at least TS forces (if enabled by strict flag) you to explicitly mark all those places. You can always revisit them later.
Case in point: I’ve written A LOT of redux-saga code and figuring out types for that was exceptionally difficult for me. Sprinkled all that with a ton of anys. Had a few bugs but not anything serious.
Finally rewriting all that slop with async-await and am really happy about it
I.e. the "I'll go back and add safety later" strategy. Somehow I never seem to get around to doing it.
I find the same thing, but only for very small throwaway scripts and the like. For anything beyond like 20 lines of code, I rapidly hit confusing cases like “is this parameter just a map, or a map or maps?” Then I add types and it makes sense again.
Also, FWIW, TypeScript is the most “lightweight” statically typed language I’ve ever used, in terms of extra ceremony/lines of code over a dynamic language. Once you get used to its type system, and embrace the structural typing ideas, I feel the overhead is super minimal. It might slow me down by ~5% on a short script over JavaScript, while dramatically improving maintainability as a codebase grows.
[citation needed]
I bet the percentage of web developers who want this are a tiny minority. There are just too many issues with this.
Static typing without the benefits of better runtime performance, soundness, strong typing etc. is basically just documentation/comments with extra steps.
Also TS is a complex, moving target. Most devs don't want to learn new fancy features every couple of months, but prefer stability guarantees. Several notable projects have moved away from TS. Even Ryan Dahl admitted that integrating Deno with TS was probably a mistake.
Meanwhile you have WASM slowly and steadily getting crucial features on a sound foundation.
I'm extremely cautious about TS and wary of the hype surrounding it.
The sad thing about the tech sector is that you can be right about something and yet still lose the hype-wagon popularity contest. Then your competitor copies your original idea... The exact same idea which they had previously claimed was inferior.
It seems that the idea was inferior purely on the basis that it wasn't their idea. As soon as they've appropriated the idea, suddenly it's the best idea in the world.
So many runtimes and tools are integrating TypeScript now, and with multiple implementations, that a real standard is necessary. It'll be much harder to evolve TypeScript because it'll have to stay backwards compatible, but it's grown to that point now, imo.
And naive me hopes for a future where in my web-app I can set a policy that any non-ts, type-incompliant code is not allowed to run.
The amount of exceptions I get in the console from terrible garbage-code outside of my control but that I have to include because enterprise is staggering. Would love to have a meta-setting which would just kill them if they can't be arsed to even have a modicum of code-hygiene (sorry for the rant)
Or if you are hoping to get the benefit of type checking in the browser itself (taking the same sweet time as tsc, but this time on every browser instead of once in the CI), then how long would you want to wait to be able to actually use the new typing functionality described in e.g. the latest TS annoucement? https://devblogs.microsoft.com/typescript/announcing-typescr... .
Because it would take a while until that would then become the standard and then become available in every browser. And you still need to provide the JS versions, because not every browser is going to support TS.
In the meanwhile you could just keep using tsc just as before and get access to new functionality immediately.
(I imagine you could run tsc in the browser right now if you really wanted to.)
Oh, no, certainly we want to keep type-checking in the pipeline, somewhere.
However, if the browser "understood" typescript, your codebase could have immediate hot-reload, without any transpilation in-between. The type-checking could then be (and already is when using something like esbuild/swc) an entirely separate process that happens independently.
Webpack's HMR is pretty good, but not having to modify the code at all to have it work in the browser, that'd be much much better :)
... the browser being able to typecheck (and reject violating code) itself is certainly something I'd love to see eventually, but fully agreed, this is not happening anytime soon.
This doesn't require the type annotations at runtime though, it's just a side effect of code being written against a static type system.
And I was replying based on what I've seen other people saying whenever the subject comes up; apologies if I misread.
Browsers doing type checking is a pretty fraught idea IMO, at least with Typescript and not some other statically typed language entirely.
To take one extreme example, a library I work on includes an API for calling a JSON RPC server. Instead of manually implementing each call, we use a proxy object that converts any method call on it to a JSON RPC call. Then layer on types so given an RPC object you know every method on it and have typed input params and output. This means you can have any number of methods without increasing your bundle size, because all the types disappear at runtime. It also means you can add your own methods if you’re talking to a server that implements custom ones by just defining types. If you shipped this to the browser with the types then it’d be a much bigger bundle than without them.
I think we disagree on the terminology but agree on the goal of the proposal
tsc does also support jsdoc though, so technically I think tsc is closer to supporting types as comments, though it's possible Flow has this also.
I think I get the distinction you're trying to make, but I don't think the nuance there is significant enough to merit differentiating the two.
And it would apply to Flow’s type annotation syntax which is also not presently treated as comments, at least for the very large subset of that syntax which overlaps with the proposal.
It refers to how the types are parsed: aside from some kind of standard start and end delimiters, the parser does not try to parse the expression-level type syntax. Type expressions are just strings of characters. This way you can have basically any syntax at all for types.
The point is that the types could be TypeScript, Flow, Hegel, or something else. The browser won't perform type checking, it will just ignore the types.
So, it is not standardising TypeScript.
> At runtime, a JavaScript engine ignores them, treating the types as comments.
The phrasing here is that the types are meaningless. They are just "comments" to the JS engine.
Years and years ago. Now it's whatever the compiler does.
Which to be fair, is evolving quite rapidly.
But they don't have it.
yeah, the ecosystem sucks, but rewarding the system incentivized to co-opt the system will actually make things worse in the long run, not better.
for example, internet explorer failed why?
IE failed because it was a horrible browser that didn’t evolve for years and was incompatible with major web standard developments. Nothing to do with typescript, an open source, best in class type system and type checker.
i’m not bullish on political strategies being technical solutions, which is the premise.
typescript has nothing to do with internet explorer, true, but is it really not obvious that it is the same tactic as a different brand? become the standard, steer the committee.
and broken in that copying code between systems requires compatibility between configurations, which should be a red flag for any language.
this node feature is primarily around disregarding typescript in favor of the underlying javascript it represents.
that reminds me of this fun article: https://www.richard-towers.com/2023/03/11/typescripting-the-...
this is still a “runs some typescript” and “not runs every typescript file”
“ At least initially in this PR no trasformation is performed, meaning that using Enum, namespaces etc... will not be possible.”
this type of nuance is the core of why typescript is a headache for any organization with more than a single codebase— javascript is portable, typescript is in theory, but not in observed practice.
Nevertheless, I don't see TS support in e.g. browsers being anything useful, as in practice all JS code deployed is already packaged somehow, so the stage to convert TS to JS (and then also checking the types..) fits that just fine. It's useful for hobbyists, but I don't that is a reason enough to come up with a standard.
https://github.com/microsoft/TypeScript/issues/30551
Which goes to https://github.com/microsoft/TypeScript/issues/9998 which captures a lot of such scenarios
And just a few releases ago there were some big problems with handling recursive types. I think most are fixed but there may still be a few around.
These are things that you run into on a daily basis if you write enough TypeScript.
I doubt we should standardize TypeScript before we have definitive solutions to all these. And I 100% agree with the parent's comment.
The type checker is immensely complex and should be left out so that other type checkers can be developed, e.g. similar to how it's for Python today.
I do think that the semantics should be standardized too, otherwise you have non-interoperable types. The goal should be that you can use two libraries together without having to make sure they use the same type-checker.
A lot of effort and money has been invested into JS engines. I wonder if making a TS native engine (which nobody has made yet) from scratch might make more sense than adapting JS engines to run TS.
What TC39 needs is a type system that limits what you can do to things that are sound, performant, and good practice. TS is the exact opposite of this.
If you need JS syntax features that Node.js doesn't support you can use tsc, babel, etc.
Because obviously unsupported features are unsupported, whether JS or TS.
This is only 'half-assed' anyway, TS will only emulate new language features on older JS target version, but not any Javascript runtime features (like new Object methods). For the latter you will still need a separate polyfill solution.
I understand that built in TS support is very nice as provided by Bun. But I feel doing this in Node is going to take a long time.
> During the transpilation process, no type checking is performed, and types are discarded
Which makes perfect sense to start out with; as typechecking with tsc is rather slow and can easily be delegated to the consumer.
(I use Deno, but also use some npms.)
In which case wouldn't V8 support TS directly without needing to transpile?
> There is a TC39 proposal for type annotations
Which links to https://github.com/tc39/proposal-type-annotations
It is a long ways off though.
that would be a lot of work
I would rather see new WASM features instead
type: module requires file extensions and does not support importing folders like most people are used to so it will not be compatible with most existing Typescript code.
Will it transform esm import syntax into require statements?
I'd prefer it break existing code to enforce correctness
There are about 100 messages in the comments on the linked PR all about the import extensions compatibility/consequences :)
There's a "roadmap doc" here https://github.com/nodejs/loaders/issues/217 that states "No .js extension for .ts files."
> I'd prefer it break existing code to enforce correctness
That seems to be the goal and intent (from TS and Node team too https://github.com/nodejs/node/pull/53725#issuecomment-22301... )
> Will it transform esm import syntax into require statements?
IIUC no
see the docs about which module system Node picks for the loaded files https://github.com/nodejs/node/blob/main/doc/api/typescript.... ("Node.js will not convert from one module system to another")
The authors trick you with reasonable examples on https://www.typescriptlang.org, but in the wild, you have these ridiculous codebases that couldn't control themselves and they have this insane ratio of multiple declaration files to actual source files and you have to ask yourself, "Are you writing software to get something actually done, or do you just like write type definitions?"
Even people who write in C++ don't go to the lengths that TypeScript users do. It's super weird and cult-like.
There are some type libraries that parse GraphQL queries and CSS selectors. They’re crazy to look at but they’re hugely helpful.
And I would wager the bugs and jank are in no small part due to the extremely complex generics/constraints.
If you mean expressiveness or consistent or soundness then no, it’s actually very bad compared to almost anything else and I think the longer it goes on the more it starts to feel like a house of cards.
The upside I guess is that whenever Safari decides to get their shit together Web Assembly is well placed to get us out of the scenario where we are forced to use JS and as an extension Typescript at all for most things and actually good language choices with reliable type systems like Dart, Kotlin and C# all become viable options.
There is no way I’d choose JavaScript over those other options in the majority of scenarios unless I was forced to.
Out of those three only Dart has nice DX story compared to JS world.
All other static languages start bottom up, simple to more complex, but end up getting boxed in by their own design. TypeScript started top down, trying to map itself on to a fully dynamic language. Never getting boxed in, just trying to 'fill' the box that is all the possibilities of JavaScript. 10 years on and TypeScript is still exciting, making significant updates and improvements.
You may have a point that Typescript would have been relegated to obscurity with all the others had it tried to start "top down" as a brand new language. There may be some truth that it is a necessity of a language to start simple in order to become accepted in the mainstream and that Typescript only made it because it rode on the coattails of a language that also started simple: Javascript.
I want to emphasize that the reason we keep running into "ceilings" is probably because of its advanced type system. Libraries and frameworks are using those type features and when we can't keep building on the type - we end up casting to unknown and reconstructing it. Which feels worse than not being able to construct that complex type at all.
In C# you can't work with optional generics because an optional reference type is different from an optional value type.
C#s poor type-inference often requires you to type out types thrice. You can't declare constants or class members with type-inferrence.
The only way to define sum-types (A | B | C) is through intefaces and I'm pretty sure they can't be sealed. Defining product-types (A & B & C) is impossible.
{name: string} & {birthday: Date}
becomes a single type with both properties.
Record types are tuple types with names instead of indexes.
The TypeScript “&” is another thing.
Eventuall they add the feature something else has, and then everyone uses it again instead of the other thing.
https://github.com/tc39/proposal-type-annotations
So maybe we'll have support in another five years
... Or maybe we'll have "Support in another five years" in five years, who knows
Enums are one of the very few things in typescript that seem to not have turned out that well, but it's relatively easy to work without them with string-literable types and such, derived from some const in case they're also needed at runtime.
I for one can can live without all these features and will be banning via eslint.