tc39 / proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
https://tc39.es/proposal-type-annotations/
4.27k stars 47 forks source link

Trojan Horse Concerns #187

Open spillz opened 1 year ago

spillz commented 1 year ago

The title is dramatic but it is at least a concise expression of how I feel about a proposal that adds an enormous amount of syntactic complexity and expressive overhead to the language with what is intended to be no runtime implications. I too value the benefits that type annotations can provide but I believe the annotation approach in this proposal suffers from a few major flaws relative to simpler annotation approaches:

  1. The proposal as specified, even if labeled strawman, clearly furthers the commercial interest of one private vendor, whether or not the parties advancing the proposal have any affiliation.
  2. The strongest use case for type annotation is for library code APIs by helping library users understand the API, reducing user errors significantly, and speeding user code development. But once a library maintainer lets that camel's nose in the tent it becomes a battle to stop it from infecting internal library code as well, taking a library from type annotations in the API to type annotations everywhere. There are huge tradeoffs in terms of time to refactor code and type brittleness of TypeScript that make types everywhere a bad idea in a dynamically typed language. Recent announcements from maintainers of prominent libraries abandoning TypeScript within their core library (or altogether) make this point.
  3. Adding these annotations is literally adding extra payload to HTML and JS files and increasing latency. This also raises the potential of securities issues, especially given that parsing out what part of the syntax is optional type info isn't always straightforward given how expressive TypeScript typing is. I'm mindful that these annotations will be added to code that could potentially end up running on browsers all over the world. That's not to say there aren't security issues in other language features but this just seems to be expanding the surface area of vulnerabilities for limited benefit relative to simpler type annotation approaches (see below).
  4. The resemblance of the proposed syntax to the syntax of statically typed languages, i.e., interweaved on the same line as functional code itself, is a cognitive trojan horse. It potentially traps developers, especially new developers who might see annotations presented as best practice, into a mentality that dynamic typing is always bad or that explicit is always better than implicit. One of the benefits of using a dynamically typed language on the web is that we can break gracefully.

Given the above, I don't believe the alternative of a JSDoc-ish syntax that would cleanly delineate annotations from functional code is being taken seriously enough in the proposal. By "JSDoc-ish" I don't mean that the types should literally be embedded in comments but, for example, a type annotation marker (for single lines perhaps "@", "#" or "~") could be added to the language that is functionally equivalent and as easy to parse as a comment at runtime but also easily distinguishable from an actual comment. That at least means you could not interweave annotations and code on the same line as proposed without actually opening and closing the annotation (e.g., /@, @/) or restricting annotations to the end of a line (a la // comments). My preference would be for annotations to always be on separate lines (or separate blocks of lines to longer defs) from functional code each demarked by a single leading character, which would then make them extremely easy to strip at runtime and easily distinguished by developers. The type annotation syntax itself should still be part of the ES spec, which I believe would be important for adoption and to avoid fragmentation. A motivation to include the syntax in the spec is that the annotations could also be used for inference inside of browser debugging tools. I've seen examples of one liner annotations prefixing lines of functional code such as methods and functions given in other Issues here that look as readable as the TypeScript version even without IDE UI hints, especially if you replaced the comment marker with another character.

If you got this far, thanks for considering an alternative viewpoint!

egasimus commented 1 year ago

I absolutely agree with the sentiments expressed here, though I will step in to uphold a slightly different viewpoint. Maybe I'm wrong and am playing right into the hands of Microsoft here, by sounding too cranky or whatever. After all, we're writing this on their forge, in a culture of development largely shaped by them and the like of them. I'm already taking steps to remove TypeScript and GitHub from my workflow so I can afford to take my chances.

The past few days are the first time I notice more people speaking up about things that I have been vocal about since day 1 of basically being tricked into using TypeScript. ("Sure, just rewrite this codebase you're struggling with to have types and we'll help you with it"... boy was I naive.)

To call the response of the TS kool-aid connoiseurs "toxic" would be an understatement. Since it is apparently beneath them to engage in good faith with arguments about the drawbacks that TS silently imposes, I can only explain their behavior as "trying to spread malicious learned helplessness virally".

TypeScript was already the trojan horse. The promises of gradual adoption and backwards compatibility were always false. The reality is that you have to drag along a whole Microsoft-friendly toolchain, up to and including a telemetry-laden IDE, and AI-based suggestions, so you would be allowed to write and run code in what was previously an open ecosystem.

(In comparison, Apple just have you buy stuff to develop for their platform - at least that's kinda honest and doesn't manipulate you into discarding your prior experience and relearning a new, janky and unpredictable platform that only pretends to be "open source"...)

Instead of seeing their changes immediately, people now have to wait for egregiously long for the toolchain to approve their code on every iteration. Thanks to - let's call 'em out-of-scope factors - until now a lot of people have ignored everything that's wrong with this "new normal".

I do not deny the benefits of adding an optional static typing layer to JavaScript. The reality is that TypeScript is not optional enough, it's a single unspecified implementation, it's EEE. However hard we may try to stay positive, the benefits that we derive from static typing do not necessarily outweigh the drawbacks of achieving it through TypeScript.

Standardizing native type syntax to JavaScript can be a way out of this deadlock. Personally, every day I'm more and more convinced that JSDoc-style comments are the way to go, since they don't require a breaking change to the syntax. (If there's going to be a breaking change, we might as well have optional runtime semantics with it.)

Besides, they encourage coders to add a comment header to every function; that is, to establish a place in the source code where they would hopefully write a couple of sentences in human language about what their function is trying to do - another good practice that so, so many people neglect.

Again, what I think you're missing here is that the trojan horse is already past the gates. The camel is in the tent - nose, hooves, tail and two heavy bags of transpilers. Developers, especially new learners, are already trapped in an inferior way of doing things, and their efforts to "make themselves right retroactively" are devastating the ecosystem even worse than the debacle that is half-hearted adoption of ES Modules.

So we might as well meet them where they are and standardize the TypeScript-like syntax, just so it's out of the hands of a single vendor with a track record of insidiously undermining open ecosystems.

Either way, I really hope something is done to break TypeScript's grip on the Typed JavaScript ecosystem. Our options are as follows:

theScottyJam commented 1 year ago

From my reading of the notes of past meetings notes, it does seem like the committee has given them strong pushback against the TypeScript-centric focus this proposal has. The champions were also asked to be open-minded to various other avenues related to bringing types into JavaScript, including being open to avenues that the README currently says they weren't planning on exploring. The README hasn't been updated since they've received their feedback.

I'm also against the proposal as it currently stands - it's not really type-system agnostic at the moment, it adds a ton of new grammer to JavaScript, and all it accomplishes is eliminating a build step, which is nice, but IMO it's not paying for itself (and it's probably not what many people had in mind when, in the survey quoted in the README and in presentations, JavaScript users were saying they wanted JavaScript to have static typing). I'm glad that, when reading through the proposal meeting notes, I was seeing these same kinds of concerns and more being echoed.

Right now, it feels like we just need to wait around until the README does get updated to show what direction change they plan to take with the proposal. From what it sounds like in here, they plan on presenting something again next meeting, so maybe we can see the proposal's README updated by then?

romulocintra commented 1 year ago

Both invited ( @theScottyJam @egasimus) to the #184

trusktr commented 1 year ago

Personally, every day I'm more and more convinced that JSDoc-style comments are the way to go

JSDoc is just too verbose. I started a new conversation of syntax that is more concise that includes both docs and types, here:

https://github.com/microsoft/TypeScript/issues/48650#issuecomment-1721578187

  • Comment-based syntax: backwards compatible and with better affordances for documentation. No compile step needed but the people who are used to looking at TypeScript might find it awkward.

If the syntax can be concise enough (f.e. like in the linked conversation) this is a decent option.

even worse than the debacle that is half-hearted adoption of ES Modules.

(Sorry, slightly off topic, but since you mentioned it), [Bun](https://bun.sh/) is not helping here by enabling freely mixing CommonJS and ES Modules in the same file.
matthew-dean commented 1 year ago

@spillz @egasimus @theScottyJam THANK YOU for so eloquently summarizing the sentiments felt by so many JavaScript / TypeScript developers.

steve-carell-thankyou

somebody1234 commented 1 year ago

tl;dr: i'm not seeing the mandatory "microsoft-friendly toolchain" here.

it is worth noting however, that TypeScript already supports JSDoc as an alternative syntax, and it's almost as powerful (if not just as powerful) as typescript itself. for what it's worth, for my personal projects i use purely jsdoc + .d.ts files, with full type safety. it's also worth noting that flow has its own types-in-comments syntax as well.

addressing the... most :

The reality is that you have to drag along a whole Microsoft-friendly toolchain, up to and including a telemetry-laden IDE, and AI-based suggestions, so you would be allowed to write and run code in what was previously an open ecosystem.

telemetry-laden IDE

firstly - it's worth noting that it's open source. i myself use a fork of vscode (not to avoid telemetry though, although that may be a bonus). secondly - it's worth noting that typescript provides a language server, so it can be (and is!) used with any IDE that supports a LSP - which is more or less every IDE nowadays. so i'd be more surprised if you can find an IDE that doesn't support typescript

AI-based suggestions

not sure this was ever needed. i'm actually not too sure what you even mean, given that i don't use any form of AI suggestions (nor do i need to)

other notes:

somebody1234 commented 1 year ago

re: the options

somebody1234 commented 1 year ago

addressing the points in the op:

clearly furthers the commercial interest of one private vendor

this is true.

There are huge tradeoffs in terms of time to refactor code and type brittleness of TypeScript that make types everywhere a bad idea in a dynamically typed language

note that this is optional, so it shouldn't be an incentive for library authors to use typescript just because it's available - nor do i think it will incentivize library authors to do so. if they want to use typescript they would already be using it, if they don't want to use it they would already not be using it.

Adding these annotations is literally adding extra payload to HTML and JS files and increasing latency.

this has explicitly been addressed in the proposal itself. note that non-minified code is several times bigger than minified code, so even if types make the code 100% bigger, i don't think its impact is as large as that of minification.

especially given that parsing out what part of the syntax is optional type info isn't always straightforward given how expressive TypeScript typing is

this is why it intentionally does not include all of typescript's syntax. more importantly, if this spec is finalized it will include a grammar, which i'd assume browser makers would be more than familiar enough with to implement without bugs. if it's that much of a worry though, i'm sure it wouldn't be too much effort to create test fixtures for this proposal.

The resemblance of the proposed syntax to the syntax of statically typed languages is a cognitive trojan horse

implying that statically typed languages are a bad thing.

One of the benefits of using a dynamically typed language on the web is that we can break gracefully.

a potential hot take: the thing is that we don't need to break gracefully. not when the only place where there is user input, is directly at the user interface. in the 90% of the application that never ever interacts with user code, being extra robust is not only "adding extra payload", it also adds noise making the code harder to maintain, and is a huge amount of unnecessary runtime slowdown if you know for sure that the inputs are always the type you say they are - because you're the only one calling those functions in the first place

dynamic typing is always bad

this may be a hot take, but i think in general dynamically typed code is much harder to reason about. note specifically that regular code is not dynamic typing - dynamic typing is constructs like these:

note that typescript is structurally typed, so things like JSON received from an API is often statically typed - (imported JSON, and regular object literals, are both statically typed. as are things like 'key' in obj checks)

explicit is always better than implicit

i feel like this implies explicit runtime type checks is better than implicit (= none at all).

spillz commented 1 year ago

Since writing the OP, I've become more sympathetic to JSDoc comments over all of the alternatives:

There are a few things that can't be done as compactly or completely as they might be done in TypeScript but for the reasons I lay out above, I consider that a feature. I agree there are pros and cons to static vs. dynamic types. I firmly disagree that one is always superior to the other. This can easily become a semantic argument once you allow any to be a "static" type in any case. Or watch me pass arbitrary objects as strings and hand decode them to avoid some gnarly type restriction, completely undoing all the supposed benefits of strict typing in the process. Or that time I accidentally swapped the order of the two numbers expected by the slice method.

In light of what already exists for JSDoc, I agree with comments above that it's hard to make a persuasive case for either a TypeScript-like syntax or yet another JSDoc-like syntax over what already exists in JSDoc. I do still see some merit in including type annotation syntax as something like an annex to the JS Spec to encourage adoption and potential future use in run-time optimization, debugging or other uses. I do see potential to further streamline and improve JSDoc, however, and to deprecate some garbage like @interface, which doesn't seem to provide anything useful beyond what @typedef already does.

As for those who want strict static typing on the web, isn't that what WebAssembly will allow for?

somebody1234 commented 1 year ago

@spillz the point of static typing is to reduce bugs though, there is no very little advantage in switching javascript to runtime typechecking, not when its engines are already so heavily optimized.

also wasm is a solution, but it isn't particularly feasible imo. the main issue is that everyone would have to include their own runtime, which is way more bloat than any amount of js could ever be. another issue is that it has to include its own gc, which would likely be slower than js' native gc. plus, wasm does still need javascript bindings to use javascript apis, and for the longest time wasm<->js execution wasn't exactly decently fast

somebody1234 commented 1 year ago

once you allow any to be a "static" type in any case

any is a typescript type so i'm not sure what you're getting at here

pass arbitrary objects as strings and hand decode them to avoid some gnarly type restriction

nobody does this, it's way too slow. typescript is heavily unsound and so there are tons of workarounds for every conceivable issue. the simplest escape hatch is type assertions - you can simply do as TheExpectedType or as unknown as TheExpectedType. it may not be completely invisible but that's the point - to make you have to think whether a type assertion is the right tool to use here. so like how you consider jsdoc's issues features, i consider type assertions as a feature too.

Or that time I accidentally swapped the order of the two numbers expected by the slice method.

not like it's not also a problem in javascript. type systems are cool but in no language do they catch when you pass the arguments wrong (except for languages with builtin verification and/or refinement types, and theorem provers)

somebody1234 commented 1 year ago

fwiw when i initially saw this proposal way back i did think it was too typescript-centric, and way too complicated. i do think there are better, more general approaches that may deviate somewhat from typescript syntax, but would be much simpler to define, for example:

this is of course hypothetical, but the point is that 20+ new grammar productions is absolutely not needed.

one issue with this syntax would be type assertions and generic parameters though... and i guess type annotations in general. so you might want an inline version of this, however note that % is already the modulo operator so maybe % isn't such a good idea after all. note also that # is not a good idea because of both private properties, and the upcoming record/tuple proposal.

also i was (still am) against this proposal not mainly because it's overkill or anything - but rather because it brings so little benefit - it only accepts a predefined subset of syntax, and it will take so long to pass that i suspect it won't even be usable in production for the better part of a decade - not that anyone will use it in production at all!

spillz commented 1 year ago

@spillz the point of static typing is to reduce bugs though, there is no very little advantage in switching javascript to runtime typechecking, not when its engines are already so heavily optimized.

I am aware that people want it for bug reduction but it has other uses too. If you hint the types, runtimes can get more of a speed bump by assuming that's the type and verifying later. It's the constant checks that can make dynamic types expensive. It can also help in debugging by auto flagging type violations at runtime. I don't know how useful that is, it just might be.

Static and dynamic types aside, what everyone hates is type ambiguity. It shouldn't be as hard as it is in JavaScript to verify that a variable is a string or a number type.

also wasm is a solution, but it isn't particularly feasible imo. the main issue is that everyone would have to include their own runtime, which is way more bloat than any amount of js could ever be. another issue is that it has to include its own gc, which would likely be slower than js' native gc. plus, wasm does still need javascript bindings to use javascript apis, and for the longest time wasm<->js execution wasn't exactly decently fast

I am just saying WASM is more realistic avenue for people who want enforced static types than trying to convert JS spec to actually enforce static typing. It's still early days for WASM. Even tho we all live with AI brain rot nowadays, human programmers are going to be around for a good while yet.

spillz commented 1 year ago

once you allow any to be a "static" type in any case

any is a typescript type so i'm not sure what you're getting at here

Any language that has a true any type is not statically typed by definition and obviously TypeScript itself is not statically typed. I was responding to the point that someone made earlier that what some people actually want is truly statically typed JavaScript and pointing out that means giving up a lot of flexibility.

pass arbitrary objects as strings and hand decode them to avoid some gnarly type restriction

nobody does this, it's way too slow.

Again, not talking specifically about JavaScript/TypeScript specifically. In C, you end up passing around lots of objects as buffers because it is too hard flexibly specify something statically in a struct that is ultimately dynamic. That eventually turns into a markup parser and sometimes into a full blown scripting language....

typescript is heavily unsound and so there are tons of workarounds for every conceivable issue.

I can't even tell what you are arguing for. You seem to to jump between defending the status quo, defending TypeScript, defending strict static typing, then saying nobody should be required to use... I think the only thing I don't see is an admission that static types won't solve all the world problems or that dynamic typing is ever OK and that is really the only point I tried to convey in my OP.

Your replies feel to me like arguing for arguments sake and generally a pretty uncharitable reading of my views so I won't keep replying to that.

somebody1234 commented 1 year ago

If you hint the types, runtimes can get more of a speed bump by assuming that's the type and verifying later.

almost all js runtimes already do this, without the need for type hints. js runtimes are fast because they've solved the constant checks. i'm pretty sure what makes them expensive nowadays is, that they always have to look out for overridden property lookups. either on the prototype, a custom getter, a proxy, maybe someone overwrote Object.keys, things like that

also note that in the proposal text again - v8 tried implementing static typing ages ago - and it failed. i'm sure the results would be quite different now, given how much v8's architecture has likely changed, but it is still worth noting that it failed once in the past.

people who want enforced static types

except that nobody really wants this, because javascript is already fast enough. almost all typescript users know that types simply don't exist at runtime, and that's fine - because they use typescript to catch errors early.

note that this proposal does not enforce any static types - in the very first paragraph of the proposal, they say (emphasis theirs):

type checker that is external to JavaScript

Any language that has a true any type is not statically typed by definition

so c# isn't statically typed :(

pass arbitrary objects as strings

re: re: this. typescript understands runtime checks for narrowing very well - 'k' in obj && typeof obj.k === 'string' gives you a type-safe type for obj.k, so even if you have no idea what the type could be, it's perfectly possible to use typescript to type-safely recover some info from the object. (as a bonus, it requires exactly zero type annotations, s it's also valid js.)

an admission that static types won't solve all the world problems

the thing is, a lot of people find static types incredibly useful. this proposal does not affect people that don't use type annotations (and in fact, can't), because javascript must be backwards compatible. "not everyone wants static types" is not the same as "nobody wants static types".

You seem to to jump between defending the status quo, defending TypeScript, defending strict static typing, then saying nobody should be required to use

i am not "jumping" anywhere, all of the above are true:

also i'm just addressing the points one by one, and i think it's only fair that i have different opinions on different topics.

egasimus commented 1 year ago

existing typescript users already use transpilers so i don't think it's the end of the world

Nice! We're talking about all JavaScript users here though. Those also already use transpilers most of the time. Technically, they shouldn't have to. That's another "optional" thing that usually isn't.

Remember when transpilers were a compatibility solution for (1) deploying code to people stuck on old versions of MSIE, and (2) bundling CommonJS code from Node back to browsers because they didn't have modules yet?

Browsers now support ESM natively, ES5 is baseline, ES6 is baseline, support for post-ES6 features is being delivered on a reasonable cadence, what makes you so attached to your dev server? The 3 layers of frameworks on top?

I believe in making JavaScript accessible and user-friendly with a minimum of tooling. It's the language everyone in the world has on a hotkey, ffs! Devs have grown accustomed to writing JS with types? Great, let's give 'em types!

Back in the sticks we used to call that "paving the cowpaths". Might've heard.

for what it's worth, for my personal projects i use purely jsdoc + .d.ts files, with full type safety.

So, what checks that the .js and the .d.ts correspond to each other? I haven't been able to find such a tool - even though type inference should be able to provide that to a reasonable extent.

So even though there's this nominal "escape hatch", turns out you have to write non-standard, non-runnable .ts to get the actual type safety. Nuuuuudge... scoot over, sanity.

Is there at least something that generates the .d.ts from the JSDoc or do you now have to maintain type definitions separately from the code, not once, but twice?

firstly - it's worth noting that it's open source.

Irrelevant. Chromium is also open source. That doesn't mean it's not someone's monoculture and enclosing the commons.

i myself use a fork of vscode (not to avoid telemetry though, although that may be a bonus).

Good for you! What percentage of your coworkers do likewise? 0% of mine AFAIK.

secondly - it's worth noting that typescript provides a language server, so it can be (and is!) used with any IDE that supports a LSP - which is more or less every IDE nowadays. so i'd be more surprised if you can find an IDE that doesn't support typescript

Ever hear what the rabbi advised the villager about the goat? That's where you are with TS, methinks.

I'm talking about needing an IDE, LSP for the IDE, TSC for the LSP, all these moving parts underneath a text editor, just to be able to write TypeScript in the first place.

The (somehow only half-working) TS LSP in my editor makes TS somewhat bearable and occasionally even useful. Sometimes, it catches errors. Sometimes, they actually have to do with whether my code is doing what it's supposed to do. I don't like this "sometimes". Static types are supposed to give you more certainty about the behavior of your program; personally, TypeScript has left me with less of it.

That's largely due to so many moving parts in the environment. And to think I went to JS because, in comparison with other scripting languages, it had the fewest...

Also, ever try to write an LSP server - or extend one? [^0]

AI-based suggestions

not sure this was ever needed. i'm actually not too sure what you even mean, given that i don't use any form of AI suggestions (nor do i need to)

Apparently it's needed, because writing TypeScript that compiles turns out to be too complex for the average developer. (Doubly so if they actually knew JavaScript previously, lol)

I prefer to use tools that work predictably. That's because I know such tools exist and have successfully used them.

OTOH, picture a new developer wants, nay, is told to "use what everyone is using", gets kitted up with the latest and greatest in janky build tooling, and gets down to figuring out how to actually program computers for a living. How are they supposed to know if their tools are actually hot messes that are misleading them at every step - when they don't even know what they don't know?

I'd also bring React/JSX/TSX into this, but this is enough of a tangent as it is, so let's not go there.

there are plenty of alternative compilers so that you don't even need to use tsc to compile - babel/sucrase, esbuild, and swc being the most popular options.

What compilers?

The other day someone posted a listicle like that somewhere. Under the heading, "look so many TS compilers", there was a table with 15-20 things. 4-5 of which actually claimed to be "compilers".

Of those, all but tsc just stripped the types :clap: I'm pretty sure about esbuild there as I use it for that exact purpose, babel can fetch me a bargepole, swc is also a type stripper as far as I saw (though it did contain Rust AST bindings, which is hella nice! in classical Rust "no var args so Web APIs get 5 different variants per function" fashion of course, clunky but somehow livable with)

And so, I've been unable to find any alterntive to tsc for the actual type checking which was the point of the exercise. Which is no surprise, given the only specification I've been able to find for TS is... v1.8 from 2016.

the status quo is fine, most people, HMR

lol

note that this is optional, so it shouldn't be an incentive for library authors to use typescript just because it's available - nor do i think it will incentivize library authors to do so. if they want to use typescript they would already be using it, if they don't want to use it they would already not be using it.

And this, folks, is how you ignore everything that makes life interesting.

If "best-of-all-possible-worlds" circular reasoning is the way to talk to people in 2023, tell me this: if the solutions to TypeScript's problems are as simple and obvious as TS apologists usually say they are, then wouldn't we have discovered them by now?

Or maybe we've got plenty of things to find workarounds for already, and one more wasn't particularly welcome.

If people knew better, they'd know better. I can only try to tell 'em, they can only try to not listen :rofl:

typescript isn't a choice that is being forced on anyone,

One could say the same about breathing, or wanting to eat.

A backwards-compatible official specification for static typing in JS, now that would truly be a choice not forced on anyone.


Meh, none of this is truly on-topic. Still, I find myself weirdly compelled, nay, forced to point out what I consider to be classic oversights in whatever argument that you're trying to make (that my problems don't make sense and the connections I draw between them don't make sense? didn't really get that... ah yes that TypeScript is a good ecosystem citizen that totally doesn't leave the door open for the rest of the gang.)

I guess I'm just an argumentative geezer and I like graffiti on the bikeshed. But for now I think I've raised enough ruckus, and would much rather focus on the syntax proposal brought forward by (I think) @trusktr, which is starting to look pretty good! JSDoc already supports an alternate, way more compact syntax, who knew? Let's standardize it, making it even more compact along the way! This way, converting a TS codebase to Standard Types won't double its size!

I feel like continuing this conversation would only cause further attrition and negative sentiment, so I'd be most thankful if you chose not to. Unless it's to suggest a tool that generates DTS scaffolds from untyped JS via type inference; or a type checker for TypeScript that isn't tsc. Those would be most welcome - and probably put to immediate use. :heart:

In return, I was going to offer you my trademark rant about the best driving instructor in town, who prevented people from learning to drive while honestly thinking he's teaching them - but you'd just tell me you didn't get the point and to start a blog. Guess I'll save it for a blog post, they love em :man_shrugging:

Peace out ✌️

[^0]: Anyone ever try to write a language server? Or extend TypeScript's? I tried to bolt type checking of TS-in-Markdown onto tsc a while back. How hard could it be, right - just turn the content that's outside the code block to blank lines, and type check the result. Couldn't even find the thing's entrypoint - as far as I remember, the LSP server used by TS is also somehow not-exactly-standard LSP? I thought I was "thinking about it wrong" (as the latest TS canard goes). Turned out Vue's language server, the only example I could find at type checking TS that's embedded in another structured text format (Vue components), monkey patches the fs methods of Node in order to be able to inject code. No other way of building atop TS. That's because the final JS bundles that are tsc.js and tsserver.js/tsserverlibrary.js/typescript.js (three 800k files which are almost the same) are helpfully wrapped in closures, so that nobody gets to poke around their "open source" and accidentally invent something that forces the boys in Redmond to miss a bonus because some KPI suddenly became completely invalid. Madness. But what would you expect from Microsoft - being able to pipe the output of one program into another like people have been doing for decades is apparently frowned upon.

lillallol commented 1 year ago

So, what checks that the .js and the .d.ts correspond to each other?

You see that is the point that a lot of people do not understand. They do not need to match. You are not manually writing .d.ts for each .js file. d.ts is used to write all the ts types and then use /**@type {import("./path/to/file.js")}*/ to import-type-annotate values in .js.

turns out you have to write non-standard

You mean non ES standard? Because TS is non ES standard in the first place. Also what I described before is already TS standard.

non-runnable .ts to get the actual type safety.

What do you mean non-runable? What I described before executes with full static type checking without the need to compile.

Is there at least something that generates the .d.ts from the JSDoc or do you now have to maintain type definitions separately from the code, not once, but twice?

There is no need to generate .d.ts from the JSDoc. There is no need to have types in JSDoc. All the types are in .d.ts files. JSDoc is used only for type import annotations. There is no need to maintain twice. In fact like this there are problems solved link. From your claims I can see that you have never made a project like this. Unfortunately the overwhelming majority of people have no clue about this way of developing. People would laugh at the context proposal if they did.

I tried to bolt type checking of TS-in-Markdown onto tsc

Just write the ts in .ts files and then special markdown comments that point to the .ts files. Then write code for merging .ts files with md. The latter I have published it in npm. But now, I never use it, since I never write code in my README.md files (and I never will).

That's because the final JS bundles that are tsc.js and tsserver.js/tsserverlibrary.js/typescript.js (three 800k files which are almost the same) are helpfully wrapped in closures, so that nobody gets to poke around their "open source" and accidentally invent something that forces the boys in Redmond to miss a bonus because some KPI suddenly became completely invalid.

You might find the following discussion interesting.

jithujoshyjy commented 1 year ago

Apologies if I sound rude but is it not possible or even easier for someone to create a vscode extension or something to convert TypeScript type annotations to JSDoc when typing out code instead of warranting an entire proposal that raises concerns from all fronts?

egasimus commented 1 year ago

@jithujoshyjy

Apologies if I sound rude but is it not possible or even easier for someone to create a vscode extension or something to convert TypeScript type annotations to JSDoc when typing out code instead of warranting an entire proposal that raises concerns from all fronts?

Hi! I don't think you sound rude, or that you should apologize.

Concerns are good, they mean there are people who care deeply about things. Caring deeply is what creates value.

Thinking in terms of "VSCode extension" is the reason this thread is called "Trojan Horse Concerns".

What do you think about a tool that converts a file from TypeScript to JSDoc once, and then you don't have one more thing running in the background that's messing with what you're typing? Some people really don't like that. I agree with those people.

I think it would be better to have this as a standalone tool (and then anyone could build an editor plugin around that). There probably even exists one already, or indeed would be very easy to write - unless you write it for VSCode only, that would make it harder.

egasimus commented 1 year ago

@lillallol

You might find the following discussion interesting.

Nice one. Happy Sunday!

spillz commented 1 year ago

@somebody1234, I'm not going to spend time addressing all of the stuff you say because it's becoming more apparent that a lot of what you write sounds like parroting other people rather than first hand experience. But a few thoughts below:

also note that in the proposal text again - v8 tried implementing static typing ages ago - and it failed. i'm sure the results would be quite different now, given how much v8's architecture has likely changed, but it is still worth noting that it failed once in the past.

I suggest you re-read the post on Google Groups. Why this failed had almost nothing to do with typing and everything to do with the early stages of optimizing for ES6. To be honest, I took most of the proposal, including its discussion of performance and linking to the Groups post, with a grain of salt because it is an advocacy piece.

What we know for sure is that strictly enforced typing will unambiguously improve performance over what can currently be achieved with JIT approaches. Whether type hints that are not enforced provide useful performance info is more uncertain but in a statistical sense it almost always has to be better to be told "these variables will almost always be these types" than to try to infer that from code alone.

If you hint the types, runtimes can get more of a speed bump by assuming that's the type and verifying later.

almost all js runtimes already do this, without the need for type hints. js runtimes are fast because they've solved the constant checks. i'm pretty sure what makes them expensive nowadays is, that they always have to look out for overridden property lookups. either on the prototype, a custom getter, a proxy, maybe someone overwrote Object.keys, things like that

It's easy to come up with an example of code running on V8 that is much slower than C. For example, a simple dot product operation on a double array of numbers:

The result with CPython and numpy, simulates the ease of gaining performance by what is essentially shifting code to static typing (numpy calls out to C routines). And this is clearly what's driving the efforts behind Mojo, which will blend dynamic types and enforced static types into a superset of Python. It's not too much of a reach to think that taking the type hint seriously in the JIT to construct vars as 2D arrays of C floats when they are hinted to be that would actually yield C-like performance on those parts of the code.

That said, I've no real dog in this particular fight. I just don't buy the argument of the proposal that there are no performance uses for type hinting, which was clearly included as a blatant attempt to cut-off inevitable objections to the proposal that would require a lot more work to evaluate properly.

people who want enforced static types

except that nobody really wants this, because javascript is already fast enough. almost all typescript users know that types simply don't exist at runtime, and that's fine - because they use typescript to catch errors early.

This was in part a reference to the survey in the proposal that asked "What do you think is currently missing from JavaScript?" which was overwhelmingly answered "Static Types". But who knows what the average programmer interpreted that to mean when they checked off that answer. It reminds me of the Henry Ford quote/aphorism "If I asked the people what they wanted, they would have asked for faster horses". What I do take seriously is the many people online who clearly think that type annotations should be rigidly enforced as a practice in all code they come in contact with and I see this proposal as advancing those interests.

spillz commented 1 year ago

So, what checks that the .js and the .d.ts correspond to each other?

You see that is the point that a lot of people do not understand. They do not need to match. You are not manually writing .d.ts for each .js file. d.ts is used to write all the ts types and then use /**@type {import("./path/to/file.js")}*/ to import-type-annotate values in .js.

But is there anything that you can put in a .d.ts file that you cannot instead put in a JSDOC, at least in terms of what's supported by the VS Code TS language server?

For example

/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

From the TypeScript JSDoc reference.

lillallol commented 1 year ago

But is there anything that you can put in a .d.ts file that you cannot instead put in a JSDOC

Yes. When I define the publicApi.ts, I do not define just types and interfaces, I use declare as well. Also there are some differences with type and interface declarations. What is @typedef defining? type or interface?

Here is as an example of a `publicApi.ts`: ```ts export declare const test : { /** * @description * Use that to define and execute the unit test. * * If the unit test does not call an `assert`ion at least once, the test is * considered skipped. * * If a unit test returns a promise that has not resolved when the next unit * is called, the library throws error and stop further execution. That * means that you should always `await` unit test that return promises. * * If an error is thrown inside a unit test, the unit test aborts further * execution, and the library continues its execution with the rest of the * unit tests. */ exec: ( specification: string, cb: () => void | Promise ) => void | Promise; /** * @description * There will be cases where you need to skip all unit tests except some of * them. For such cases do the following: * * * execute `usesOnly` before any `test` and `describe`, to convert all * `test.exec` to `test.skip` * * add `.only` to the unit tests you want to have executed * * *** * * In all other aspects, `test.only` acts like `test.exec`. */ only: ( specification: string, cb: () => void | Promise ) => void | Promise; /** * @description * Use that to skip the execution of the unit test. */ skip: ( specification: string, cb: () => void | Promise ) => void; } /** * @description * Use that to make assertions in the unit tests. An assertion that fails, * throws error. */ export declare const assert : IAssert & { not : IAssert }; type IAssert = { /** * @description * Asserts that the two values are references using operator `===`. */ reference : (value1 : unknown,value2 : unknown) => void, /** * @description * Asserts that the two values are copies. Equality is derived for * primitives using the operator `===`. */ clone : (value1 : unknown,value2 : unknown) => void, /** * @description * Asserts that the provided `cb` throws with the provided error message. */ throw : (cb : Function, errorMessage ?: string) => void, } /** * @description * You can use that to group tests. */ export declare const describe : { exec : ( specification: string, cb: () => void | Promise ) => void | Promise; } /** * @description * It makes `test.only` unit tests to be executed and `test.exec` unit tests to * be treated as `test.skip`. * * It throws if it is executed after at least one `describe` or `test` has been * executed. */ export declare const usesOnly : () => void; //#region customization /** * @description * Provide a callback to be executed each time `describe` or `test` executes. * The use case is to create your own custom, human readable specification. */ export declare const onSpecificationPartGenerated : ( cb : (specificationPart : ISpecificationPart) => void ) => void; /** * @description * Each unit `test` and `describe`, generate a specification part on their * execution. */ export type ISpecificationPart = { specification: string; /** * @description * The number of ancestor `describe`. */ depth: number; type: "exec" | "only" | "skip"; /** * @description * * `null` is only for the case of `describe`. * * `Promise` is for the case the `test` or `describe` callback returns a * promise. */ state:"failed" | "passed" | "skipped" | Promise<"failed"|"passed"|"skipped"> | null | Promise; /** * @description * This is the error message of the failed unit test. */ errorMessage?: unknown; /** * @description * `null` is for the case of describe */ assertionCount : number | null | Promise; }; /** * @description * If a unit test has not called this function at least once, then it is * considered skipped. * * If you want to use a different assertion library than the one provided by * the context library then you have to make it use this function. * * `assert` is using this function. * * @todo * think about making this function not public, and if there is a need publish * a new minor version making it public. */ export declare const incrementUnitTestAssertionCount : () => void; /** * @description * Use that to reset the internal state of the context library. If called inside * `test` or `describe`, will throw. * * The internal state is defined by: * * * `setTestTimeout` * * `usesOnly` * * `onSpecificationPartGenerated` * * The use case is, that you might not want the internal state of a file of unit * tests, to affect the other files. @TODO * */ export declare const resetInternalState : () => void; //#endregion ```
somebody1234 commented 1 year ago

nintpicks re: that perf comparison: iterators are slow, this is a free 2x speedup:

function dotProduct2(points) {
    let sum = 0;
    for(let i = 0; i < points.length; ++i) {
        const point = points[i];
        sum += point[0] * point[1];
    }
    return sum;
}

indirection is slow, this is another free 2x speedup:

function dotProduct2(points) {
    let sum = 0;
    for(let i = 0; i < points.length; i += 2) {
        sum += points[i] * points[i + 1];
    }
    return sum;
}

along with a change in the generation code:

let points = Array.from({ length: 200000 }, () => Math.random());

bringing it down to roughly 3x slower than c (3.5ms, vs 9ms in node). which is... almost as fast as numpy on my machine

while this is a tangent, it's still worth pointing out that a ton of the slowdown here is not caused by dynamic typing being slow, so much as it is caused by javascript being slow.

spillz commented 1 year ago

nintpicks re: that perf comparison: iterators are slow, this is a free 2x speedup:

You're missing the point. The 1D array you've flattened to is an edge case that V8 is JIT optimized for so I deliberately chose an example where it isn't. Once you have real world, more complex code those flattening tricks are still there but much harder to refactor to. In contrast, the C version of the 2D array just works as fast as a flat float array without any tricks because under the hood it still is.

spillz commented 1 year ago

But is there anything that you can put in a .d.ts file that you cannot instead put in a JSDOC

Yes. When I define the publicApi.ts, I do not define just types and interfaces, I use declare as well. Also there are some differences with type and interface declarations. What is @typedef defining? type or interface?

Ah, interface. I forget it exists because I avoid using it at all costs. I just use class, which I find has better performance because class info is available at runtime for JIT optimizations. There's a related technique that also has bad performance of using nested functions to define class instances as an end run around the horror of actually having to define a class that seems to be a holdover from the ES5 days.

But yes it's true that @interface isn't supported currently as a valid JSDoc annotation in TypeScript (see here) and, yes, @typedef is defining a type not an interface.

spillz commented 1 year ago

Apologies if I sound rude but is it not possible or even easier for someone to create a vscode extension or something to convert TypeScript type annotations to JSDoc when typing out code instead of warranting an entire proposal that raises concerns from all fronts?

Not rude at all!

Here's one such attempt: https://github.com/futurGH/ts-to-jsdoc

There's also the idea (not mine) of having a TypeScript editing mode for JavaScript in an IDE where the JavaScript code is reformatted to look like TypeScript in the editor but get's saved as JavaScript with JSDoc on disk.

trusktr commented 1 year ago

this has explicitly been addressed in the proposal itself. note that non-minified code is several times bigger than minified code, so even if types make the code 100% bigger, i don't think its impact is as large as that of minification.

Also well-written code probably means also a considerable amount of prose in comments that far outweigh the amount of type annotations. If you're shipping to production for high traffic sites with lots of users, especially for in cases when you care about click-through rate, etc, you will be stripping all comments in a build step, regardless.

I believe:

This concern is thus not really a concern, because if we're worried about production speed, then we're already compiling, and we are not writing minified commentless code.

JSDoc types are waaaaaaay more verbose too.

convert TypeScript type annotations to JSDoc when typing out code instead of warranting an entire proposal that raises concerns from all fronts?

Not rude at all!

Here's one such attempt: https://github.com/futurGH/ts-to-jsdoc

There's also the idea (not mine) of having a TypeScript editing mode for JavaScript in an IDE where the JavaScript code is reformatted to look like TypeScript in the editor but get's saved as JavaScript with JSDoc on disk.

Of course this is all possible, but is library-specific, and does not satisfy needs for people who have no (or want no) build tools (myself included).

trusktr commented 1 year ago

There is no need to generate .d.ts from the JSDoc. There is no need to have types in JSDoc. All the types are in .d.ts files. JSDoc is used only for type import annotations.

@lillallol I understand what you mean, but can you link to a project that follows this pattern exclusively? I would like to dig into the details and see how ergonomic it is, including things like generics on classes, etc.

How would you define a generic class in Foo.js with types in Foo.d.ts?

spillz commented 1 year ago

this has explicitly been addressed in the proposal itself. note that non-minified code is several times bigger than minified code, so even if types make the code 100% bigger, i don't think its impact is as large as that of minification.

Also well-written code probably means also a considerable amount of prose in comments that far outweigh the amount of type annotations. If you're shipping to production for high traffic sites with lots of users, especially for in cases when you care about click-through rate, etc, you will be stripping all comments in a build step, regardless.

But if you're effectively always stripping all type annotations anyway, why do you need type annotations in the JS spec, or more importantly, in the runtime? Especially if they are non-functional.

JSDoc types are waaaaaaay more verbose too.

There's some discussion in an issue that basically rebuts this difference in verbosity in the sense that you can give a one liner JSDoc that uses TS syntax and also use .d.ts files. What is true is that you can't intersperse the types in the declarations in JSDoc like you can in TS, but some of us consider that a feature.

Not rude at all! Here's one such attempt: https://github.com/futurGH/ts-to-jsdoc There's also the idea (not mine) of having a TypeScript editing mode for JavaScript in an IDE where the JavaScript code is reformatted to look like TypeScript in the editor but get's saved as JavaScript with JSDoc on disk.

Of course this is all possible, but is library-specific, and does not satisfy needs for people who have no (or want no) build tools (myself included).

It's exchanging a TS build step for an edit time "TS view". And this would be general purpose in the sense that any code that has valid JSDoc could be presented to the user in an editor as if it was TS. I suppose you could build some UI tooling in the editor to help quickly spec out the types for JS files that lack the needed JSDoc.

spillz commented 1 year ago

There is no need to generate .d.ts from the JSDoc. There is no need to have types in JSDoc. All the types are in .d.ts files. JSDoc is used only for type import annotations.

@lillallol I understand what you mean, but can you link to a project that follows this pattern exclusively? I would like to dig into the details and see how ergonomic it is, including things like generics on classes, etc.

How would you define a generic class in Foo.js with types in Foo.d.ts?

Take a look at Svelte. Example ComponentConstructorOptions:

https://github.com/search?q=repo%3Asveltejs%2Fsvelte%20ComponentConstructorOptions&type=code

trusktr commented 1 year ago

But if you're effectively always stripping all type annotations anyway, why do you need type annotations in the JS spec, or more importantly, in the runtime? Especially if they are non-functional.

Not everyone is. Some people don't use build tools, and will never need to use build tool, let alone want to.

There's some discussion in an issue that basically rebuts this difference in verbosity in that sense that you can give a one liner JSDoc that uses TS syntax and also use .d.ts files.

Can you please show us how to do this with a class Foo<T> defined in Foo.js with Foo.d.ts for type definition support?

lillallol commented 1 year ago

Can you please show us how to do this with a class Foo defined in Foo.js with Foo.d.ts for type definition support?

Please give an example on .ts so that I will try to convert it.

trusktr commented 1 year ago

Please give an example on .ts so that I will try to convert it.

class Bar {bar = 123}

abstract class Foo<T> {
  abstract method(): T
}

class Test extends Foo<Bar> {
  method() { return new Bar() } // ok
}

class Test2 extends Foo<Bar> {
  method() { return new Object() } // type error
}

new Foo<Bar>() // type error

Can you please also show a working example project with these classes in their own files (Foo.js, Bar.js, etc) with fully working intellisense and type checking, that we can clone and open in VS Code?

spillz commented 1 year ago

Can you please also show a working example project with these classes in their own files (Foo.js, Bar.js, etc) with fully working intellisense and type checking, that we can clone and open in VS Code?

I don't know about abstract class because I never use that myself (and the TS community has a pretty strong aversion to class stuff for reasons unclear to me so it's probably not supported with JSDoc currently). But you can do the equivalent with an interface (technically it's a typedef but you can define it as an interface in a .d.ts if you wanted). This is pure js that will flag the error correctly in VS Code.

//@ts-check

class Bar {bar = 123}

/**
 * @typedef {{method: ()=>T}} Foo<T>
 * @template T
 */

/**@implements {Foo<Bar>}*/
class Test {
  method() { return new Bar() } // ok
}

/**@implements {Foo<Bar>}*/
class Test2 {
  method() { return new Object() } // type error
}
spillz commented 1 year ago

But if you're effectively always stripping all type annotations anyway, why do you need type annotations in the JS spec, or more importantly, in the runtime? Especially if they are non-functional.

Not everyone is. Some people don't use build tools, and will never need to use build tool, let alone want to.

That describes me too, btw. I have been working with JS and TS intensively for the last couple of years but come from a Python background and before that C/C++/Java/Pascal/Assembly. The move from C/C++ to Python was incredibly liberating -- even with its massive standard library of untype safe code it all works pretty darn well because it is well documented with sources that can be easily browsed and is blessed by namespaces that makes it absolutely clear where things come from (a massive source of C/C++ errors). From there it was an easy hop to ES6 flavored JavaScript. I love dynamically typed languages for rapidly putting prototypes or version 1.0s of things together where the types are constantly evolving and so it makes no sense to me to pre-define them up-front.[1] And that is most of what I do, which reflects my perspective on this proposal. But I also do see the benefits of TS and have used it for quite a few things now. There's something to be said for the fact that unlike JS, TS or JS with JSDoc almost always knows what object you are working with and can tell you what properties it has, which saves a few brain cycles. So in my longer lived projects my (relatively new) preference is to use JS with JSDoc rather than TS.

Whether or not you are using a build system--and presumably you are at least for your minify step--if you want the real benefits of type annotations, whether it's true TS or JS with JSDoc and @ts-check, you have to use a language server and therefore you are using the TS build system because tsc is used by the server to infer the types. The language server is what flags the problems in your code literally as you type them. Getting that feedback inside your editor is a much less expensive context switch for your brain than waiting for compile or, worse, runtime. Furthermore, TypeScript type inference, where for example let t = 1 annotates t as a number automatically, is actually a backwards step for code readability if you aren't using a language server, especially once you are working with more complex objects, unions etc.

[1] Your mileage may vary even in early stage prototyping. I may write up some additional notes to defend my point 4 of this issue at some point.

lillallol commented 1 year ago

@trusktr Please provide a real world example that does not produce inconsistent .js .d.ts.

theScottyJam commented 1 year ago

How do you do "as" with jsdocs? And new Map<string, number>()?

trusktr commented 1 year ago

@trusktr Please provide a real world example that does not produce inconsistent .js .d.ts.

I don't exactly know what you mean here. I'm just writing ts code.

You previously said we can simply write types in .d.ts files, and mostly plain JS in .js files, with only minimal /** @type {import('...')} */ to bring in the types into the JS files. In a lot of cases, we can.

My above example shows valid type-checked TypeScript code, here it is in TypeScript playground.

I wanted to know, using the technique that you suggested is easy to use, how you would write it.

But I think the answer is, we can't! It won't be convenient!

@spillz's answer works to an extent. Now try it with this version of the Foo class instead:

abstract class Foo<T> {
  abstract method(): T
  realMethod() { return doSomethingWith(this.method()) }
}

playground

and it starts to get more complicated. Assume that all of the classes in my example have many real properties and methods, regardless if they are abstract.

I bet it will not be an easy conversion to plain JS + declaration files, but I'd love to be wrong.

One thing is for sure: with type comments, there's no issue.

spillz commented 1 year ago

How do you do "as" with jsdocs? And new Map<string, number>()?

//@ts-check
/**@type {Map<string, number>}*/
let a = new Map();
a.set('foo', 1); //OK
a.set('bar', 'fizz'); //error

let cat = Math.random()>0.5? 1:'dead'; //null|number
let god_does_not_play_dice = /**@type {number} */(cat)*2; //OK, asserted a number first -- parens matter!
let quantum_cat = cat*2; //error, cat is null|number
trusktr commented 1 year ago
let god_does_not_play_dice = /**@type {number} */(cat)*2; //OK, asserted a number first -- parens matter!

Yeah, casting is fairly verbose with JSDoc. Note, the parnthesis are required (and Prettier even knows this so it won't strip them).

And here's an issue for non-null casting with JSDoc:

There was one more, I can't find it, but it was comparing JSDoc to ! (JSDoc obviously waaaaay more verbose). Definitely cumbersome in JSDoc. Because you have to cast it to the type you know it is, and there isn't a non-null cast in JSDoc specifically.

spillz commented 1 year ago

@trusktr

@spillz's answer works to an extent. Now try it with this version of the Foo class instead:

After digging a bit more, I believe that ideally your revised abstract class would be decorated as follows in JSDoc.

//@ts-check

class Bar {bar = 123}

/**
 * @abstract @class
 * @template T
 */
class Foo {
    /** @abstract @type {()=>T} */
    method() { throw Error("You must define method")};
    realMethod() {
        const ret = this.method();
        console.log('do something with subclass value', ret)
        return ret
    }
}

/**@extends {Foo<Bar>}*/
class Test extends Foo {
    method() { return new Bar()} // ok
}

/**@extends {Foo<Bar>}*/
class Test2 extends Foo {
    method() { return new Object() } // type error
}

new Foo(); // should be a type error but currently is not because neither @abstract is enforced

const b = new Test().realMethod() // ok

In VS Code, the generic part of this works but the @abstract parts do not for the simple reason that it hasn't been implemented in tsc -- see TS issue discussion with the key quote "I believe current usage is hugely depressed by the existence of Typescript, since people who want this feature likely want all the other OO decorations that Typescript adds, like public and private. I'm going to leave this open because things may change in another decade or two."

I don't think this sort of thing could be easily implemented in a combined JS and .d.ts because it mixes implementation and declarations. I didn't really try though.

But honestly, this sort of stuff is why I was happy to leave C++ behind and a classic example of a solution in need of a problem. Like it's cute that realMethod can be defined without knowledge of T but really, who cares?

One thing is for sure: with type comments, there's no issue.

Except for the small detail that there is no current implementation of JavaScript with TS annotations. There would certainly be less work to fix the biggest gaps in JSDoc than complete and implement anything resembling the current TypeScript-based annotation spec.

lillallol commented 1 year ago

@theScottyJam Lets start with as.


Using as[link] and !.[link] is a bad practice since they override the type checker. It is true that the override is safer than //@ts-ignore, but that does not change the fact that it is an override. These:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
function liveDangerously(x?: number | null) {
    // No error
    console.log(x!.toFixed());
}

can be replaced with these

const myCanvas = document.getElementById("main_canvas");
if (!(myCanvas instanceof HTMLCanvasElement)) throw Error();
function liveDangerously(x?: number | null) {
    if (x === null) throw Error();
    console.log(x!.toFixed());
}

leading to safer static type checking. It is the ability to use as and !. that leads to loss in static typing, not the inability to use them.

Finally if people suspect that as and !. will lead to significant performance increases due to skipping run time checks, then they have to prove it with benchmarks on minimally reproducible examples from real world projects, for which the usage of EcmaScript, instead of other programming languages, is justified. Until then, as and !. is a bad pattern that reduces static type safety.


Notice that the examples that I have picked are not random. They are the examples used in the TypeScript handbook when as and !. are introduced.

You still want to use as for reasons? Fine do that then:

/**@type {import("./from/some/where.js").IMyType}*/
//ts-expect-error
const newCastedValue = oldValue;

or the inline one that is suggested by @spillz.

@trusktr

Yeah, casting is fairly verbose with JSDoc. Note, the parnthesis are required


There is a common misconception that there is an intrinsic need for an extra pair of parenthesis[link].


Interestingly enough you see different answers from the TypeScript maintainers on that issue.

And here's an issue for non-null casting with JSDoc:

You can use this:

/**@type {import("./privateApi.js").IIsNullable}*/
export const isNullable = (v) => v === null || v === undefined;

/**@type {import("./privateApi.js").IFn}*/
export const fn = (a) => {
  if (isNullable(a)) throw Error();
  a;// hover over me, vscode says I am a number
}
//./privateApi.ts
export type IIsNullable = (v: unknown) => v is (undefined | null);
export type IFn = (p : undefined | null | number) => void;

And before some people claim that we need as for const assertions:


Instead of using const type assertion[link] you can do the following:

In production, minifiers (worth their salt) will get rid of asConstArray, hence the argument that this function might have an impact for performance critical applications, is not valid.


Regarding the abstract class example: The abstract class gets compiled to an empty class. I am just trying to understand why would anyone extend an empty class, so that I can make a correct conversion?

Finally regarding the verbosity argument. Make projects with the way I suggest, and if you feel it is a problem, then fine. My experience is that it is not.

theScottyJam commented 1 year ago

I agree that as should often be avoided. I disagree that it's universally a bad practice.

Just one example:

const STATES = ['RUNNING', 'COMPLETE', 'ERROR'] as const;

function isValidState(state: string): state is typeof STATES[number] {
  return (STATES as readonly string[]).includes(state);
}

I know this particular use of as is related to a limitation of the language itself, but that's how it is for many of the "good" uses of as.

lillallol commented 1 year ago

Here is how to do that without as:

/**@type {import("./privateApi").IAsConstArray}*/
export const asConstArray = (array) => array;

export const STATES_AS_CONST   = asConstArray(['RUNNING', 'COMPLETE', 'ERROR']);
/**
 * Will be minified away without causing any issues in the prod code.
 * @type {import("./privateApi").IReadonlyArrayOfStrings}
 */
export const STATES_AS_STRINGS = STATES_AS_CONST;

/**@type {import("./privateApi").IIsValidState}*/
export const isValidState = (state) => STATES_AS_STRINGS.includes(state);
//./privateApi.ts
import { STATES_AS_CONST } from "./index";

export type IAsConstArray = <const T extends readonly unknown[]>(array : T) => T;
export type IReadonlyArrayOfStrings = readonly string[];
export type IIsValidState = (state : string) => state is (typeof STATES_AS_CONST)[number];
theScottyJam commented 1 year ago

Hmm, that seems fairly unfortunate that you're basically exporting the same value twice, the only difference is how specific the type is, just to make an includes check easier. Granted, it's also unfortunate that TypeScript requires some sort of hack-around (either with as, or a second variable like you did) to use this includes() function in a very normal way.

Anyways, here's one more example use-case for as - this time I'm using it to narrow a type (naughty!) instead of expanding it.

function tryConvertToSearchParams(obj: unknown): string | null {
  if(typeof obj !== 'object' || obj === null) return null;
  for (const entry of Object.entries(obj)) {
    if (!Array.isArray(entry)) return null;
    if (entry.length !== 2) return null;
    if (!(0 in entry) || typeof entry[0] !== 'string') return null;
    if (!(1 in entry) || typeof entry[1] !== 'string') return null;
  }

  return new URLSearchParams(obj as Record<string, string>).toString();
}
spenserblack commented 1 year ago

Throwing in another usage for as :wink:

<a id="foo" href="#">Foo</a>
const anchor = document.querySelector('#foo') as HTMLAnchorElement;

The writer knows that anchor will never be null, and also what type of HTML element it will be, but the type checker doesn't know this. This is a scenario that's better handled by testing than type checking IMO, and as HTMLAnchorElement is a simple shortcut to both appease the type checker and get better completions in your editor.


The abstract class gets compiled to an empty class. I am just trying to understand why would anyone extend an empty class, so that I can make a correct conversion?

If you look at the compiled JS it might look weird, but the reason is that the abstract class defines the methods that must exist on the concrete class. The developer should have confidence that any class that extends the abstract class will have these methods. Here's a simple recreation of Rust's Result as an example:

export abstract class Result<T, E> {
  abstract unwrap(): T
  abstract unwrapErr(): E
}

export class Ok<T> extends Result<T, never> {
  constructor(private v: T) {
    super();
  }

  unwrap(): T {
    return this.v;
  }

  unwrapErr(): never {
    throw new Error('unwrapErr called on Ok')
  }
}

export class Err<E> extends Result<never, E> {
  constructor(private err: E) {
      super();
  }

  unwrap(): never {
    throw this.err;
  }

  unwrapErr(): E {
      return this.err;
  }
}

function queryApi(): Result<{ status: number }, { error: string }> {
  throw new Error('Not implemented for example')
}
const response = queryApi();
const { status } = response.unwrap();

We don't immediately know if the return from queryApi is Ok or Err. But, with type checking, we do know that it will have the unwrap() method. One could make Result an interface, but then you lose instanceof Result.

I do acknowledge that this can all be done with one concrete Result class, but I hope this still serves as a reasonable example regardless.

A bit off topic Even without TypeScript, an empty class can be useful. Here's a recreation of Python's `getattr`. ```javascript class Undefined {} // IIRC I've seen also this done with `const myUndefined = (class Undefined {})();` // in the wild. export function getattr(obj, key, defaultValue = Undefined) { // I'm going to be lazy and assume obj is always an object if (key in obj) return obj[key]; if (defaultValue !== Undefined) return defaultValue; throw new Error(`obj does not have '${key}'`); } ``` Why create our own `Undefined` instead of just using `undefined`? So we can do this: ```javascript getattr({}, 'foo', 'default'); // 'default' getattr({}, 'foo', null); // null getattr({}, 'foo', undefined); // undefined getattr({}, 'foo'); // Error! ``` The private `Undefined` class is a trick that provides an easy and (IMO) readable way to differentiate between the user explicitly passing `undefined` as the third argument vs not passing a third argument at all.
zaygraveyard commented 1 year ago

@spenserblack

A bit off topic Why not just use a `Symbol` instead of the `Undefined` class (or any object for that matter)?
spenserblack commented 1 year ago

@zaygraveyard

off-topic Because I've spent too long writing in other languages and forgot that `Symbol` was available :sweat_smile: Guess I need to do better about making sure idioms from one language I use don't bleed into another.
theScottyJam commented 1 year ago

Anyways, here's one more example use-case for as - this time I'm using it to narrow a type (naughty!) instead of expanding it.

Let me actually answer my own question here.

That code snippet could be redactores to use a userland type guard, and that would remove the need for the "as", at least for this specific example.

However, using a userland type guard doesn't fix the root issue. The reason "as" can be bad, is because we're making assumptions about the type of the object without relying on Typescript's type inference to figure those details out for us. With a type guard, we're still making the exact same assumptions, we've just moved where the assumptions are happening - into the type guard function. Perhaps type guard functions are the more idiomatic, fine, but there are cases where writing out a proper type guard can add a fair amount of code bloat, and can be a slightly unnecessary performance cost. But, I think I'll save you from doing further examples :), I think I'm seeing the general picture of how one would live without "as".