-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Trojan Horse Concerns #187
Comments
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:
|
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? |
Both invited ( @theScottyJam @egasimus) to the #184 |
JSDoc is just too verbose. I started a new conversation of syntax that is more concise that includes both docs and types, here: microsoft/TypeScript#48650 (comment)
If the syntax can be concise enough (f.e. like in the linked conversation) this is a decent option.
(Sorry, slightly off topic, but since you mentioned it),Bun is not helping here by enabling freely mixing CommonJS and ES Modules in the same file. |
@spillz @egasimus @theScottyJam THANK YOU for so eloquently summarizing the sentiments felt by so many JavaScript / TypeScript developers. |
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 + addressing the... most :
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
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:
|
re: the options
|
addressing the points in the op:
this is true.
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.
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.
this is why it intentionally does not include all of typescript's syntax.
implying that statically typed languages are a bad thing.
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
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
i feel like this implies explicit runtime type checks is better than implicit (= none at all). |
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 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 As for those who want strict static typing on the web, isn't that what WebAssembly will allow for? |
@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 |
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
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) |
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 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! |
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.
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. |
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.
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....
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. |
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 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.
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):
so c# isn't statically typed :(
re: re: this. typescript understands runtime checks for narrowing very well -
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".
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. |
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.
So, what checks that the So even though there's this nominal "escape hatch", turns out you have to write non-standard, non-runnable Is there at least something that generates the
Irrelevant. Chromium is also open source. That doesn't mean it's not someone's monoculture and enclosing the commons.
Good for you! What percentage of your coworkers do likewise? 0% of mine AFAIK.
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? 1
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.
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 And so, I've been unable to find any alterntive to
lol
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 🤣
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 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 🤷♂️ Peace out ✌️ Footnotes
|
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
You mean non ES standard? Because TS is non ES standard in the first place. Also what I described before is already TS standard.
What do you mean non-runable? What I described before executes with full static type checking without the need to compile.
There is no need to generate
Just write the ts in
|
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. |
Nice one. Happy Sunday! |
@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:
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.
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.
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. |
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
From the TypeScript JSDoc reference. |
Yes. When I define the Here is as an example of a `publicApi.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>
) => void | Promise<void>;
/**
* @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>
) => void | Promise<void>;
/**
* @description
* Use that to skip the execution of the unit test.
*/
skip: (
specification: string,
cb: () => void | Promise<void>
) => 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>
) => void | Promise<void>;
}
/**
* @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<null>;
/**
* @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<number>;
};
/**
* @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 |
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. |
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. |
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. |
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. |
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.
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). |
@lillallol How would you define a generic class in |
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.
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.
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. |
Take a look at Svelte. Example https://github.com/search?q=repo%3Asveltejs%2Fsvelte%20ComponentConstructorOptions&type=code |
Not everyone is. Some people don't use build tools, and will never need to use build tool, let alone want to.
Can you please show us how to do this with a |
Please give an example on |
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? |
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
} |
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 [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. |
@trusktr Please provide a real world example that does not produce inconsistent |
How do you do " |
I don't exactly know what you mean here. I'm just writing ts code. You previously said we can simply write types in 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 abstract class Foo<T> {
abstract method(): T
realMethod() { return doSomethingWith(this.method()) }
} 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. |
//@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 |
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 |
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 I don't think this sort of thing could be easily implemented in a combined 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
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. |
@theScottyJam Lets start with Using 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 Finally if people suspect that Notice that the examples that I have picked are not random. They are the examples used in the TypeScript handbook when You still want to use /**@type {import("./from/some/where.js").IMyType}*/
//ts-expect-error
const newCastedValue = oldValue; or the inline one that is suggested by @spillz.
There is a common misconception that there is an intrinsic need for an extra Interestingly enough you see different answers from the TypeScript maintainers on that issue.
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 Instead of using const type assertion[link]
In production, minifiers (worth their salt) will get rid of 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. |
I agree that 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 |
Here is how to do that without /**@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]; |
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 Anyways, here's one more example use-case for 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();
} |
Throwing in another usage for <a id="foo" href="#">Foo</a> const anchor = document.querySelector('#foo') as HTMLAnchorElement; The writer knows that
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 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 I do acknowledge that this can all be done with one concrete A bit off topicEven without TypeScript, an empty class can be useful. Here's a recreation of Python's 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 getattr({}, 'foo', 'default'); // 'default'
getattr({}, 'foo', null); // null
getattr({}, 'foo', undefined); // undefined
getattr({}, 'foo'); // Error! The private |
A bit off topicWhy not just use a |
off-topicBecause I've spent too long writing in other languages and forgot that |
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". |
yeah, well... the language service provider for jsdoc (and javascript in general) in vscode is typescript, after all |
@lillallol according to that issue you linked they are required. That's all I was saying. |
Abstract classes can have non-abstract members that get inherited. I ended up figuring it out here:microsoft/TypeScript#48650 (comment) I was trying to use |
Regarding the example you have posted link it all boils down to separating the implementation and the type of an abstract class. Unfortunately TypeScript does not provide such a feature (yet). It is my understanding that this a matter of support and not something that intrinsically can not be supported. Here is how I would do it if such a feature was supported:
export class Bar {bar = 123};
/**@type {import("./index").IFoo}*/
export const Foo = class {
//This method will not be needed if TypeScript support abstract member in
// IFoo
method() {
throw Error();
}
realMethod() {
const ret = this.method()
console.log('do something with subclass value', ret)
return ret
}
}
/**@type {import("./index").IFooBar}*/
const FooBar = Foo;
class Test extends FooBar {
method() { return new Bar() } // ok
}
class Test2 extends FooBar {
method() { return new Object() } // type error
}
new FooBar() // type error
const b = new Test().realMethod();
// does not type error because abstract member in IFoo is not supported yet by
// TypeScript, although it will throw error in runtime
(new Test()).method()
import { Bar, Foo } from "./test";
export type IFoo = abstract new <T>() => {
//It all boils down to TypeScript supporting abstract members
abstract method() : T
realMethod() : T
};
export type IFooBar = typeof Foo<Bar>; |
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:
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!
The text was updated successfully, but these errors were encountered: