> TypeScript's type system is over-complex since it tries to be a superset of JS.
This is exactly why it got so popular, and TypeScript support in the JS ecosystem is so good. It's easy to just strip the TS metadata and you're left with working JS. This is why projects like esbuild managed to ship TS support so quickly (without type-checking).
Good luck doing that with a new language.
I for one am "all in" on TypeScript, with all its shortcomings it's just the best of both worlds, and so easy to get on board.
TypeScript's biggest flaw, IMO, is that it can't (or chooses not to- I don't understand anything about JS-adjacent build systems and how they work) understand the difference between "code I write" and "code someone else wrote".
I'm not going to beat the dead horse of how unsound TypeScript's type system is. We all know that most of that is intentional because it would just be too inconvenient to work with existing JavaScript code (Aside: think about what this implies about existing JavaScript code. This makes alarm bells go off in my head).
However, why should I have to deal with an unsound type system in my new, fresh, TypeScript-only code?
I'm not saying it's simple or criticizing anybody's work, but it would VASTLY increase my opinion of TypeScript if I could enable some kind of 'truly-strict-new-code' flag that would error on unsound code in the current project and maybe just warn when I call unsound code from a dependency.
Because, honestly, I'm truly disappointed that the one language that finally has a chance to unseat JavaScript has such a poor type system that it can't even do basic sub-type variance correctly no matter how many flags you set. Out of all of the statically typed languages I've used, TypeScript has helped me the least when it comes to type errors.
I was just thinking about this. TypeScript’s main flaw is that it’s a full superset of JavaScript, which is a full superset of old JavaScript with weird conventions (null vs undefined, == vs ===). Furthermore, its type system is so strong it can type practically any JavaScript, but at the cost of strong typing.
I wish there was language that was basically JavaScript/TypeScript, but it gets rid of backwards-compatible JavaScript edge cases and conventions (like, no more undefined, no more {} == ‘0’, no more weird function ‘this’), removes advanced types and makes the type system sound, and (just in case) can actually check typecasts and non-null assertions at runtime. JavaScript and TypeScript are honestly great languages, but they are great languages with major flaws in the name of backwards-compatibility.
Even with strict typing enabled, I write a surprising amount of bugs which ultimately come down to type errors. And not only that but silent type errors, because TypeScript’s casts and not-null-assertions aren’t checked. So often I find that a mistyped or null value managed to slip through a type guard without the compiler saying anything.
Totally agree. That's kind of what I was going for when I described the hypothetical "true-strict" mode: don't allow the code I'm writing to do bad/unsafe stuff.
And yes, I really wish that TypeScript's non-null assertions actually included a runtime check. Type casts can't really do anything at runtime without a definition of what "being type Foo" actually means, since the types are erased, but you can definitely check if something is null or undefined and throw an error.
I think it would be fair to have the non-null assertion do a runtime check, and if you want the non-null assertion with no runtime code, just use a type cast.
I add a bunch of tests at the top of all JS function - To make sure the parameters are correct. There is no good reason to not write these tests. You can throw friendly errors, and bugs will be detected early.
I also use automatic tests, and most of the time it's the tests inside the functions that detect the error - because the tests runs the code. It's much more simple then trying to prove correctness before the program haven't even run once.
Backward compatibility is bridge to whole JS ecosystem. If there was way use “unsafe” old js code within newer language that would be great. But it would bring another set of annoying problems.
I can't get behind the idea that a type system is either completely sound or completely useless.
Surely preventing 99% of issues (or 90%, or 80%) is good?
I'm also not aware of too many real-world instances of unsoundness in TS that has caused problems. Obviously I don't know your specific scenario, but it's possible there are ways to work with TS (instead of against it) to get a good level of type safety.
I think the gradual typing is just too permissive... It's not the same feel you get with type inference. Not to say that a unsound type system is useless. If you're coming from dynamically typed languages it will certainly feel safer, but if you come from a more strict type system it still feels like just a linter. In my experience.
Can you link to where we previously discussed TypeScript? I don't remember that, and can't find it anywhere in my comment history (looked back to 2019).
Also, I didn't mean to misinterpret what you said. Sorry about that.
Yikes. I'm so sorry. I mistook your username for another one. That was incredibly rude of me to react like that without at least double-checking.
I had a long and frustrating back-and-forth with someone else some time ago where I pointed out what I believe to be several glaring issues with TS's type system (like the fact that `readonly` properties on object types don't actually guard against writes in most situations) and every one of my criticisms was met with the equivalent of "You're saying a type system has to be 100% perfect or it's completely useless". And even though it's the internet and I should know better, it just irritated the hell out of me.
Again, I'm really sorry for jumping at you like that. I think I'm traumatized over arguing about TypeScript... lol
Is it even possible for the program to know at compile-time if the types will be violated at run-time? Even if you declare data from other sources 'not mine' this still seems like it would be impossible to do.
The type system can force you to handle both cases - the data being in the expected & typed form, and the data not being in the expected form.
You would have a function like this:
parseJsonString : String -> Result Error Person
Where Result is a value that is either an Error or a Person.
To get the Person value out, the type system forces you to also do something with the Error if it exists. Not handling both possibilities in some way is a type error.
> Is it even possible for the program to know at compile-time if the types will be violated at run-time?
I mean, that's kind of the whole point of static typing, right?
If you're speaking to TypeScript/JavaScript, specifically, then yeah, the compiler can't guarantee anything because someone can hop into the running program and start modifying objects as they please (at least for web page code).
But the compiler could still make the weaker guarantee that "the code that I can see is soundly typed". And for 99% of us that's effectively the same thing.
As long as the data is from within the system, yes. If you're trying to use data from an external source (api request, external json file etc) than no, but that's true of most type systems.
If you correctly type the response of that request then the answer becomes yes again.
Maybe I misunderstood, but I thought the person I was responding to was suggesting having the option for sound typing enforced in typescript, at compile time. Because if it's at run-time... well the run-time is Javascript, so there's of course no typing there. This isn't really on Typescript
There are however javascript libraries which allow you specify object schemas and throw errors when mismatches occur. Perhaps the suggestion then is to build some shared layer between the TS compiler and V8 so that similar schemas can be autogenerated by the TS compiler and enforced at run-time.
There is generally no typing in run time. One of the practices in writing compilers is to skip type checks during run time, because you know beforehand that no type errors are possible. That is type erasure.
If you're interested, lookup existential types. Very interesting stuff.
FWIW the lack of explicit variance annotations (and probably many other TS unsound design choices) are not so much related to supporting old untyped JS, but rather language design decisions that have to do with pragmatism and type system complexity.
I can't find it now but I have a vague recollection Hejlsberg talking about the explcit variance annotations explicitly, how those were a huge can of worms in C# and they didn't want to bring them to TS. Unlike Flow, by the way, which does have explicit variance handling and doesn't suffer from the beforementioned foot gun (but does have its own foot guns of course).
Yes and no. I do think that there are only two correct ways for a type system to do generic user-defined types: you either give us variance markers or you make all user-defined generic types invariant (Swift does this).
Personally, I hate the latter, but not as much as I hate unsoundness.
Yeah I agree, that's not what I'd consider good variance rules either.
I guess my point was more about the reasons behind these weird rules. AFAIK interop with existing JavaScript was not the primary reason for doing them this way, if that's any consolation.
> I guess my point was more about the reasons behind these weird rules. AFAIK interop with existing JavaScript was not the primary reason for doing them this way, if that's any consolation.
Are you sure? Because there was an essay written by the TypeScript devs when they released whatever version that introduced the strict-function-variance flag explaining why they "couldn't" make it also apply to class methods, and it boiled down to breaking too much existing JavaScript code.
That's not proof that the original variance choice was also due to JavaScript compatibility, but I feel like it's a very strong suggestion that it was probably a big motivator.
I'm glad that it wouldn't work in Flow. I'm sure it wouldn't work in most/all of the other statically typed compile-to-JavaScript languages. I just hate that none of them won out over TypeScript, so they're basically all irrelevant- Flow included.
> TypeScript's biggest flaw, IMO, is that it can't (or chooses not to- I don't understand anything about JS-adjacent build systems and how they work) understand the difference between "code I write" and "code someone else wrote".
Arguably no language in the world makes that distinction and Microsoft's "Just My Code" debugger tools are fantastic but use a lot of heuristics that you generally tell get as many false positives as false negatives, which is likely why it would always be a toggle.
That said, I do think Typescript could make a better distinction about types that come in from code it has compiled itself (directly seen) and types that come by way of definition files with no code attached ("hearsay" types). (Again, I can't think of any language today that trusts code versus symbols files so differently, so this would be a new thing.)
Often I want it for the: I'm looking at the JS or its documentation right now and it would let me call it this way, but for some reason the Community types are too strict and I don't want to file a DT PR and get into some philosophical discussion about authorial intent versus JS as written versus documented examples and which is "most true" so I'm just going to have to cast this API to any for now and call it a day.
But I can also see the point in: don't give me strictness errors outside of the fence of code you are compiling for me right now in this project. And maybe more unsoundness tools, such as option to treat all return results as `unknown` and force defensive coding when using outside APIs. In general I think `unknown` isn't used enough in community types (it is still "too new").
Though `unknown` is also maybe too defensive for a lot of commmunity types, too.
Also, if you want to code that defensively, you could always replace community types .d.ts files yourself with ones that use a lot more `unknown`.
> Arguably no language in the world makes that distinction
That may or may not be precisely true, but there are languages that do things that are in that same ballpark. For example, Kotlin on the JVM "knows" when you're calling Java code and it handles certain things differently (null) than if you were calling Kotlin code- even if the code is from pre-compiled libraries.
And considering that I already have a tsconfig file that describes my project and applies rules to only the code in my project (IIRC, there's a toggle to control whether the TS compiler analyzes/compiles your node_modules), I don't see why it would be such a leap to just have an event-stricter --strict flag that didn't allow known-unsound code in my project's files.
> But I can also see the point in: don't give me strictness errors outside of the fence of code you are compiling for me right now in this project. And maybe more unsoundness tools, such as option to treat all return results as `unknown` and force defensive coding when using outside APIs. In general I think `unknown` isn't used enough in community types (it is still "too new").
>
> Though `unknown` is also maybe too defensive for a lot of commmunity types, too.
>
> Also, if you want to code that defensively, you could always replace community types .d.ts files yourself with ones that use a lot more `unknown`.
That's all a little intense, IMO. There's a clear difference between trusting a third party library's API/types and having the compiler help you catch bugs in your own code. If I call a third party function in my code and the documented types all check out. Then, if I experience a runtime type error, that's a bug in the library, not a bug in my code. This can happen in any language with escape hatches like type-casting and it's not appropriate, IMO, to guard against that. Put another way: if that is something that is a concern, then you shouldn't be using a third-party library.
And it works in both directions. You can "easily" (I mean, this is JS afterall so this heavily depends on the degree of toolchain hell chosen) move to the TS toolchain and rename your files to have the .ts extension and you have a valid TS project and benefit from the inferred types for your code. Type annotations can then be added later on as you go. And the syntax everybody already knows stays valid.
This is a huge benefit compared to something like ReScript because it makes adoption really easy. With the obvious downside of not having nice features like pattern matching.
I agree that TS eases the transition from JS, but I'd qualify your statement by saying it's not the toolchain that's a source of headache.
Ideally, one should compile TS with `--strict`, otherwise there's little difference with JS. With this in mind, it's quick and easy to switch to TS tooling. Afterwards, one can incrementally adopt stricter typing, culminating in adding `--strict` to your `tsconfig`.
In reality, since `--strict` is false by default, I'm concerned people erroneously assume that their JS code is type-safe after the first step, when it really isn't. In other words, it's no different than JS, except in name only!
The problem with the add types later approach is that in the end you have half baked .ts files left and right. And it becomes increasingly difficult to know when something compiles if it's because it's correct or because there is an any somewhere.
Yeah, agreed. Ideally this wouldn't happen like that. Temporary solutions like a half-migration have a tendency to stick around for a lot longer than anybody anticipates. But I think it is still better than not having any types at all, if only for having type information in development.
> It's easy to just strip the TS metadata and you're left with working JS.
Compared to what? JSDoc made it even easier, you didn't even need to strip anything, as the "types" were just comments. This comes with its own problems, who want to program with comments? But it didn't require any changes to use for us who want nothing to do with TS.
Yeah, duh. The argument is that there is a group of people who wants types and write libraries. Before they used JSDoc, which is effortless to use from JS, as it's just actually vanilla JS. Some of those started using TypeScript now for everything, including libraries. If I want to use those libraries, I need to fork the project and rewrite them to vanilla JS, something that was not needed before. Or find a different library/write it myself.
You actually don't, that's the nice part. NPM Typescript modules are shipped as transpiled js modules with d.ts files. Those d.ts files contain the type informations for consuming TS projects but they are completely optional and you can simply ignore them if you write a plain js project.
You can see this in the babel project, for example. It is written in typescript but used by a lot of js projects that don't care (and don't have to care) about this.
It sounds like you either don't use npm and are trying to use libraries' source files or you don't actually have this issue. This doesn't and can't happen with npm.
> If I want to use those libraries, I need to fork the project and rewrite them to vanilla JS
Why? If you want to just use, npm packages give you “compiled” vanilla JS and type definition files that you can ignore. Alternatively you can compile them yourself, if you vendor them in.
TypeScript supports providing types via JSDoc comments[1]. This gets you use all the IDE language features you'd get using TS, including highlighting type errors. I actually use this all the time, since I have plenty of scripts that are complex enough that I want type checking but not complex enough to warrant a compilation step.
There's also Google's Closure Compiler[2], which uses JSDoc for its JS type annotations.
Corollary: we should invest in systems that allow us to mechanically verify correctness of comments as readily as type checkers do with type specifiers.
>It's easy to just strip the TS metadata and you're left with working JS.
This actually touches on one of ReScript's supposed unique selling points amongst the functional compile-to-JS languages. Namely that the JS its compiler generates is so clean and readable that, if you end up deciding you don't want to bother with ReScript any more, you can just take the output from the final time you ran the compiler and treat that as a plain JS codebase to continue working on. Exactly how true that is, I don't know.
This is exactly why it got so popular, and TypeScript support in the JS ecosystem is so good. It's easy to just strip the TS metadata and you're left with working JS. This is why projects like esbuild managed to ship TS support so quickly (without type-checking). Good luck doing that with a new language.
I for one am "all in" on TypeScript, with all its shortcomings it's just the best of both worlds, and so easy to get on board.