![]() |
|
![]() |
| The syntax from the perspective of type stripping has been relatively stable for more versions of Typescript than it was unstable. You had to reach all the way back to 1.5 in part because it's been very stable since about 2.x. The last major shift in syntax was probably Conditional Types in 2.8 adding the ternary if operator in type positions. (The type model if you were to try to typecheck rather than just type-strip has changed a lot since 2.x, but syntax has been generally stable. That's where most of Typescript's innovation has been in the type model/type inferencing rather than in syntax.)
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 |
![]() |
| The “as” expression is not valid JavaScript anyway, so the default rule for implicit semicolon does not apply. A grammer for type expressions could define if and how semicolons should be inserted. |
![]() |
| I don't know a lot about parser theory, and would love to learn more about ways to make parsing resilient in cases like this one. Simple cases like "ignore rest of line" make sense to me, but I'm unsure about "adversarial" examples (in the sense that they are meant to beat simple heuristics). Would you mind explaining how e.g. your `as` stripping could work for one specific adversarial example?
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... |
![]() |
| Most parsers don't actually work with "lines" as a unit, those are for user-formatting. Generally the sort of building blocks you are looking for are more along the lines of "until end of expression" or "until end of statement". What defines an "expression" or a "statement" can be very complex depending on the parser and the language you are trying to parse.
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... |
![]() |
| You would also have to update your compiler. I guess you could phrase this as: you can't update your TS versions independently from your node.js version. But that's probably not an issue. |
![]() |
| > With a couple exceptions (like enums)
Apart from a clueless engineer attempts them, it's been working out fine to pretend they don't exist. |
![]() |
| So many different pairs are available. I like Asian corner brackets 「」and French guillemets « ». The angle brackets 〈〉 are popular in CS/math papers I think, though they might be confused with <>. |
![]() |
| It's definitely the right choice for Typescript. You could have gone the Scala route and used [] for generics, but that is so heavily used in ts/js as arrays it would not have made any sense. |
![]() |
| Flow (by Facebook) used to be fairly significant in the JavaScript several years ago, but right now it's somewhat clear that TypeScript has won rather handily. |
![]() |
| > and in that case there's nothing unsound about ts since it won't allow you to do so
Consider this example (https://www.typescriptlang.org/play/?ssl=10&ssc=1&pln=1&pc=1...):
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.
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:
It is not possible to write an analogous example in Haskell using `head`. |
![]() |
| > there's no accepted definition of what an unsound type system is
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... |
![]() |
| There's nothing arcane or particularly theoretical about soundness. It means that if you have an expression of some type, and at runtime the expression evaluates to a value, the value will always be of that type.
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. |
![]() |
| Oh, Closure Compiler is such a throwback. I still remember staring at the project page on Google Code. Isn't it like two decades old or even older by this point? Is it still alive? |
![]() |
| This can give you some hints of the current status of closure compiler:
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. |
![]() |
| I don’t know what axe you have to grind, but TypeScript is firmly in the hands of the community now. There’s not much Microsoft could do to change that. In what way would it be rent-seeking? |
![]() |
| Flow tries to be sound and that makes it infinitely better than TS where the creators openly threw the idea of soundness out the window from the very beginning. |
![]() |
| When a language's type system is sound, that means that if you have an expression with type "string", then when you run the program the expression's value will only ever be a string and never some other sort of value.
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. |
![]() |
| You can configure tsconfig.json to read JSDoc and error on invalid types so that you effectively get the same behavior as writing typescript. |
![]() |
| There's not much connection. Typescript's record types aren't sound, but that's far from its only source of unsoundness, and sound structural typing is perfectly possible. |
![]() |
| Yeah I start projects by explicitly typing `any` all over the place and gradually refining things, so every type that's specified is explicit and checked, I'm really enjoying that style. |
![]() |
| Combine this with an eslint config that nudges you about explicit any, and the typescript compiler option to disallow implicit any, and you're well taken care of. |
![]() |
| I don't find TypeScript to be burdensome when rapidly iterating. Depending on how you've configured your dev environment you can just ignore type errors and still run the code. |
![]() |
| I just checked and you are correct, in node it only installs the compiled version.
Apparently you can only view the uncompiled source code in deno since it natively supports typescript. My bad |
![]() |
| I would love to ship my source code (.ts) to npm. But Typescript team was very much against this, as there'll be tsconfig issues and other performance issues. But still fingers crossed. |
![]() |
| > adding a second one for CJS
Nobody is arguing for that. Once you ship ESM, you can continue shipping ESM. In Node 22 you can even require() ES modules (with an experimental flag, at the moment) |
Consider for example: `foo < bar & baz > ( x )`. In TypeScript 1.5 this parsed as (foo (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.