Discussion: TypeScript Output
GeoffreyBooth opened this issue · 88 comments
Similar to how the CoffeeScript compiler outputs JSX, it could output TypeScript source code. This could then be piped to the TypeScript compiler (or Babel’s TypeScript plugin) for type checking before being further transpiled into runnable JavaScript. This would provide an alternative to Flow for type annotations in CoffeeScript, and potentially better compatibility with other projects that use TypeScript. It could also support better code hinting in supported environments, similar to what Visual Studio Code provides for TypeScript.
I’ve started a wiki page that I invite anyone interested to contribute to, to consolidate all the syntax additions that TypeScript adds to JavaScript that we might potentially want to support in CoffeeScript’s output. For example, type annotations such as const foo: number = 3. I think the first step is to flesh out this page to see what all of TypeScript’s unique constructs are, to get a sense of the scope of the challenge.
Once that’s done, there are two broad approaches to implementing TypeScript output from CoffeeScript input:
-
Add new syntaxes to CoffeeScript that can be converted to the various TypeScript syntaxes, similar to how JSX was added. This would enable TypeScript output to be added without requiring a breaking change, and using the existing compiler.
-
Make breaking changes to the syntax to add support for all the TypeScript things we want to support. This would essentially require a new file format, e.g.
.tcoffee, and either a fork of the compiler or a dramatic rewrite of the existing one.
For example, the TypeScript code const foo: number = 3 can’t be implemented in CoffeeScript as foo: number = 3, because foo: number = 3 is already valid CoffeeScript; it transpiles to the JavaScript {foo: number = 3}. The CoffeeScript syntax would need to be something like foo:= number = 3 (or some other symbol(s) besides :=), to use syntax that doesn’t already parse today.
If the list of desired TypeScript syntaxes that folks add to the wiki page isn’t too long, and we can come up with acceptable non-breaking ways to support all of them, then the first option (add to the existing compiler) is viable. Otherwise the second option (.tcoffee) will be the only way. And of course it’s an open question as to whether either approach is worth the effort.
If people don’t mind, let’s please not flood this thread with suggestions for syntaxes, like better ideas for my := example. We can find a place for that, such as a new wiki page or an extension of the existing one. See also #4918; cc @jashkenas @lydell
Just as a general, fairly strongly held desire — I do not want core CoffeeScript to add syntax for types, or TypeScript. I feel like moving in that direction is the opposite of the spirit of what CoffeeScript was trying to accomplish in the first place.
But that doesn't mean it's not totally useful, and fair game for a fork or sister project.
For prior art, see TypedCoffeeScript: https://github.com/mizchi/TypedCoffeeScript
I feel like moving in that direction is the opposite of the spirit of what CoffeeScript was trying to accomplish in the first place.
Yes, arguably it is. My perspective is that I work at a big company, and many big companies are flocking to TypeScript. It might get to the point where if I want to be able to keep using CoffeeScript at work at all, I’ll need to write CoffeeScript that integrates well with TypeScript. It’s sort of like what we went through with JSX: if CoffeeScript didn’t support JSX, it wouldn’t have full support for React, which is kind of a big deal since React is the most popular frontend framework. If CoffeeScript doesn’t support TypeScript output, there are certain developers who won’t be able to use CoffeeScript. That troubles me.
At this stage in the project I feel like the top priority is maintaining and growing our community; CoffeeScript has long ago accomplished the philosophical goals it set out to achieve, perhaps far beyond anyone’s wildest expectations (see ?. in ES2020). There’s some risk in both directions: adding complexity to bring in or keep certain developers might turn away others who value CoffeeScript for its simplicity. You also can’t argue “well if you don’t want type annotations just don’t use them,” since their very existence in the language will require some familiarity for CoffeeScript developers reading other CoffeeScript code. So I have sympathy for both sides, and I’m far from decided that supporting TypeScript output in the compiler is the way to go. I think first I want to see just what that would look like if we were to attempt it: just how many syntax additions would we need? That alone might push us in the direction of .tcoffee, even if they could all be accomplished without breaking changes. But first let’s do our research.
I've been thinking about this for years. Unlike Jeremy I have never seen static type-checking at odds with CoffeeScript. This then really poses the question: What value do I see in CoffeeScript? After all the advancements in ES6, the remaining value has been syntax. So CoffeeScript has great, whitespace significant syntax, is expression-based, but lacks type-checking and a second crucial tool of today - pretty-printing. I've debated this here: https://xixixao.github.io/dilemma/ (all my opinions, might inaccurate and outdated). The conclusion I got to was that the best course of action was to bring the better syntax to ES6, instead of porting type-checking and pretty-printing to CoffeeScript. The result was https://xixixao.github.io/lenientjs/ .
This is why I'm commenting here, as Lenient is an exhaustive approach to whitespace significant syntax for ES6 and its typed variants. It could come in handy if you need to find syntax that supports both TS and CS-like syntax. Needless to say I don't think it's possible to do this without huge, breaking changes to CS syntax.
(of course Lenient has the additional huge advantage of being able to use it directly on an ES6 codebase, if the editor support was good)
So CoffeeScript has great, whitespace significant syntax, is expression-based, but lacks type-checking and a second crucial tool of today - pretty-printing.
CoffeeScript already supports type checking via Flow: https://coffeescript.org/#type-annotations. Obviously that’s not the same as TypeScript, but it’s not a complete lack of support either.
CoffeeScript 2.5.0+ can be pretty-printed in Prettier via https://github.com/helixbass/prettier-plugin-coffeescript.
What about to enable compiler plugins for parsing (tokenizer, lexer) and output?
So we don't need to fork coffee when new ideas come.
What about to enable compiler plugins for parsing (tokenizer, lexer) and output?
So we don't need to fork coffee when new ideas come.
Yes, that would be great. That would also solve the problem of every new idea needing to avoid being a breaking change.
The downside though is that in a project expecting plugins, developers would lose the ability to know what the intended output of a particular .coffee file is without also looking at the compiler plugin configuration. For example if someone creates a plugin that changes CoffeeScript scope to be block-scoped rather than function-scoped (see #4985), there's no way to know that that's in effect from reading just the .coffee files themselves. This moves us closer to how Babel is, especially for users who have enabled non-standard or Stage-0 plugins, and that's not necessarily a good thing. However the alternatives (forking CoffeeScript or never getting the new feature) aren't appealing either. Perhaps a new file extension, like .ccoffee (customized CoffeeScript) could serve as a tipoff that the reader needs to review the project compilation configuration, rather than assuming that it's 'vanilla' CoffeeScript.
Anyway that's an entirely separate feature, one that we might need to implement if there's no way to add TypeScript support without breaking changes; but I think it deserves its own thread.
I like the idea of use an extension to define which plugin to use. This will be perfect to typed coffee. However i believe the user may have a good reason to use a plugin to change the compiler behavioral and the output itself, for some task or target, without losing the compatibility with vanilla CoffeeScript.
So, what i mean is: Extension is probability the better way to enable a plugin, however a CLI argument to globally apply a plugin may steel be useful. If the user wants to change the compiler behavior, it is they benefit and responsibility.
I was once huge coffeescript fan. I still prefer the syntax, but the world has changed a lot since the introduction of coffeescript. Most of the features have been incorporated into EcmaScript, and now TypeScript is almost becoming the de-facto standard, whether you like it or not. I have to admit that the TypeScript tooling is excellent, and it's hard to get back to old way after using it for a while.
I would love to see CoffeeScript continuing it's life as alternative syntax for TypeScript. That way it could benefit from the huge momentum of TypeScript ecosystem, while offering an unique benefit – the syntax, for us who appreciate it.
Would a simpler syntax for flow comments achieve the goals of this discussion? I'm certain there's tooling to generate .d.ts files from those.
TypeScript supports reading types from JSDoc comments, which CoffeeScript already supports. I've been meaning to write a section in the docs explaining this; if anyone wants to beat me to it please feel free. I think this is probably the best solution possible at the moment, and we should definitely keep looking into alternatives.
I just did some experimenting with JSDoc and coffeescript using meteor. This is what I found:
- Adding types to coffeescript with JSDoc is surprisingly easy.
- Just because you added the JSDoc types to CS doesn't mean that your TS will get them. If I convert my JSDocumented CS files to js (with comments intact) and then import that into TS(x) I get the typings. If I import the CS the code works exactly the same but no typings. @GeoffreyBooth: is that something you can fix, should I write an issue on meteor/meteor?
- This won't give you the VS Code IDE goodness of TS while working with CS (that would be super nice, I really really want that a lot)
2. If I import the CS the code works exactly the same but no typings.
I’m not sure what this means. The TypeScript compiler doesn’t support CoffeeScript files, that much is clear, so you always have to have .js files (with JSDoc comments) for it to read.
3. This won’t give you the VS Code IDE goodness of TS while working with CS (that would be super nice, I really really want that a lot)
This would be very nice. I think what’s needed is for the JS files to be autogenerated while you work, and put in a place where tsc expects to find them. This is more a tooling configuration issue, I think.
I assume @JanMP's goal would be able to write .coffee (or .tcoffee or whatever) files and have Meteor automatically translate them via CoffeeScript + TypeScript, in particular for type checking. This feature list clarifies some of the limitations of the existing Meteor typescript module — in particular, while it uses tsc, it doesn't apparently offer checking; and it currently doesn't (but could) compile all files together to do cross-file type checking. But this suggests it'd also be possible to modify it to run coffee first, by writing a new Meteor module. (On the other hand, I don't yet understand how the existing one works. This directory doesn't seem to be where the code actually lives.)
Alternatively, and beyond Meteor, it'd be nice to create a ctsc script that supports .coffee/.tcoffee files and builds either .js files via coffee and then runs tsc for type checking. This would be an easy side project, but would make it more practical to use the existing JSDoc approach to writing TypeScript in CoffeeScript.
Incidentally, for the non-type-annotation features of TypeScript listed on the wiki, such as interface and type declarations, presumably a workaround for now is to wrap these in back-ticks (provided you later use Babel or tsc to remove these TypeScript commands for the final js code)? I remember using this workaround for import() function calls, back when CoffeeScript didn't support them.
I must say I'm excited by the possibility of adding (nicer syntax for) TypeScript compatibility to CoffeeScript, ideally with a thin layer similar to how JSX got added (and ideally also not even requiring a different file extension). I will keep you posted on any progress I make...
I started a branch that adds basic type annotation support. For example:
i ~ number
i = 5
i = 'hello'
j ~ number = 10
zero ~ -> number
zero = -> 0
f ~ (i ~ number) -> number
f = (i ~ number) ~ number -> i+1
g = ->
i ~ number
i for i in [0..10]generates the following TypeScript:
var f: (i: number) => number, g, i: number, j: number, zero: () => number;
i;
i = 5;
i = 'hello';
j = 10;
zero;
zero = function() {
return 0;
};
f;
f = function(i: number): number {
return i + 1;
};
j;
g = function() {
var i: number, k, results;
i;
results = [];
for (i = k = 0; k <= 10; i = ++k) {
results.push(i);
}
return results;
};TypeScript handles this output and reports the error on i = "hello".
Currently, the branch can use the := notation that @GeoffreyBooth suggested, or another notation that I came up with and like, which is binary ~. This does introduce backwards incompatibility: x ~ y used to parse like x ~y (implicit function call), but now a space is forbidden after the ~, which is exactly how unary/binary operators + and - behave. (x + y is an operation, while x +y is an implicit function call.) Fortunately ~ is a pretty rare unary operator and in all existing test cases (including CoffeeScript's source) never has a space after it.
Currently supported types include identifiers (number, string, etc.), function types ((...) -> ...), array types (...[]), and object types ({key: type, key?: type}), but many other types need to be added (e.g. object types and | unions).
I plan to add support for Assignments during type declaration are now supported.j ~ number = 5 (making type assignments assignable).
Note that the new notation allows a user to declare a local variable that has the same name as a parent scope (i in g above). I personally think this is a feature, but if it's viewed as not sufficiently CoffeeScripty I can remove it fairly easily.
It's definitely still a work in progress. There are probably still some bugs as I continue to figure out the parser, and many more features to add. I also don't support the AST yet.
I could use some guidance on the best way to proceed. If people want to collaborate on this, they could submit PRs against my branch. I could also start a draft PR here if that would be helpful and not too noisy (I believe they still generate email notifications on every push). I guess it depends how much those watching this repo would like to know about advances on this branch vs. just being told there's a semi-finished product. But it might be nice to have a dedicated thread to discuss the approach, unless this issue is the place. If there's interest, we could start a typescript branch on this repo and I could submit a series of PRs against it, like the recent AST extension. In any case, I invite collaboration, suggestions, tips, bug reports, guidance, etc.
I started a branch that adds basic type annotation support.
This is very impressive! Great work!
A few preliminary thoughts:
-
I’m wary about breaking backward compatibility. If the only breaking change is to use
~, it doesn’t seem worth it (since it doesn’t strike me as dramatically better than:=). If we end up needing several other breaking changes, like to support thetypeandinterfacekeywords, orenums, then we should explore something like.tcoffeeto “opt in” to this parsing mode rather than bumping to CoffeeScript 3 for this. Though a separate parsing mode would potentially make the parser quite complicated; would we need twoparser.jsfiles generated? -
A good resource to get some target examples for what we should support is the TypeScript Playground examples. These would provide good tests for our TypeScript support, both in terms of our grammar supporting all these things and also in terms of comparing our generated output with theirs.
-
In terms of how much of TypeScript we need to support before we ship any support, I think “everything” is way too high but “bare minimum” is too low. I think we need to support at least enough to know what our intended final support level will be (i.e., all but one of those Playground examples? Which ones?) and to know whether that will require breaking changes or not. If we need breaking changes to ship support for the TypeScript features that we intend to support, we need to determine that as early as possible and come up with a plan for how to address that (e.g.
.tcoffee, a fork of the compiler, a major version bump, etc.).
Nice work! Personally I'm in favor of breaking backward compatibility in exchange for first-class type support with nice syntax, which does not feel a compromise or afterthought. CoffeeScript does not have much to lose in current situation.
Just a quick note, that AFAIK simple variable typing with primitives alone doesn't bring much value, since the typescript compiler is smart enough to determine the types from initial values, although I've no idea if that works with var or only with const. That being said, I would not mind moving to const while breaking the backward compatibility.
@GeoffreyBooth Thanks for the quick positive feedback! Here are some responses / further comments:
- In testing, I found one reason we might want a
.tcoffeeor.tcsextension:tscreally wants the filename to be.ts, refusing to allow types in a.jsfile (as generated bycoffee -c). So at the minimum we probably want to change the output extension when the input extension is different. - I agree that it would be best to avoid breaking changes; I only figured this one would be small enough that it's worth considering. I have a preference for
~, partly because Mathematica and Pascal use:=for assignment (roughly CS's=) andx := number = 5feels a little weird, but I am not wedded to~, and that's why this branch already supports both (and each required a bunch of changes). Maybe there's also a third notation that is less loaded than:=that feels right and avoids breaking incompatibility. (I don't think there are any other single-character symbols though.) - I feel like the notation decision is not one I should make — it should be the maintainers' decision or even a public vote. And as you say, maybe it's best to make the decision after we have a more complete picture of whether we'd need to make more words into keywords for the other features. (I suspect this might actually be the case...
type t = ...already has a CS meaning namelytype(t = ...), but it needs to translate into the slightly different TStype t = .... If only JS supported implicit function calls!) - But on the technical side, here is what's possible: It should actually be easy to change the meaning of
~depending on the parser mode (e.g. input extension or command-line flag): all that would be needed is to change the lexer's output from the new value (~) to the old value (UNARY_MATH); then the grammar will treat it exactly how it used to, effectively disabling the typing rules (unless we keep the:=or other alternative). So that is actually an option. - I believe it would be similarly easy to introduce new keywords only when TypeScript mode is enabled (via input extension or command-line flag), by lexing
typeeither toTYPE(new) orIDENTIFIER(as usual). - Thanks for the playground link! That is a helpful starting point. We'll probably want to convert them to corresponding CS with some desired notation. Perhaps a new wiki page with specific code we want to support in a minimum viable product?
- Speaking of minimum viable product, I agree that we need enough to be useful before releasing anything. I've written very little TypeScript (though I've used Flow a fair amount), so I'm not an expert. I would guess type declarations,
typedefinitions, andasoperator would be enough to be interesting, though perhapsinterfaceand/ordeclareshould also be on the list. But it might also make sense to have a plan for all/most of the features (syntax and parser wise) before we commit to going down this path with an actual release, with an unreleased but testable branch meanwhile. (Incidentally, I added a few more TS features I didn't know to your wiki page, which I discovered while reading the handbook.)
@jholster Thanks also for your feedback!
- While I understand your point, it's also not good to alienate existing CS coders if we can avoid it easily. I for one have 22,000 lines (!) of CoffeeScript that I am actively running and maintaining in three services for online meetings/teaching, and wouldn't want a complicated upgrade process. (However, none of them use the
~unary operator.) Sadly I saw a few projects leave CS when Meteor (for technical reasons) forced them to upgrade to CS2 and it was difficult to upgrade to the newclassmodel; so we need to take care to avoid this needlessly. On the other hand, something likes/~ */~/g(but actually dealing with quoting and such) would be a pretty simple upgrade path here. - TypeScript does automatic typing with
vardeclarations, not justletandconst. (I just tested thatvar x; x = 5; x.startsWith('x');correctly finds a type error.) So we're good there. - Definitely the intent is to support compound types! I just started here, and I'm hoping that adding the full range of types will be a relatively easy addition, barring grammar ambiguities. (I'm just now adding array types, and that is requiring some lexer changes to avoid ambiguity between
x ~ string [0]andx ~ string[]. Nothing backward-incompatible though.)
- In testing, I found one reason we might want a
.tcoffeeor.tcsextension:tscreally wants the filename to be.ts, refusing to allow types in a.jsfile (as generated bycoffee -c). So at the minimum we probably want to change the output extension when the input extension is different.
This is probably way too ambitious, but instead of outputting TypeScript we could output JSDoc annotations. Then tsc and other tools would be able to read the type definitions from those, from .js files. I feel like this is likely a lot of extra work for minimal benefit, like how we decided to output JSX as JSX rather than converting it to React or other function calls, but it’s an option. It probably also has lots of its own issues, in terms of the edge case TypeScript features that JSDoc annotations don’t support.
One other thing to consider is --transpile. Babel already accepts TypeScript as input, and I bet Babel could be configured to treat .js files as TypeScript if told to. It probably wouldn’t type-check them, though, which kind of defeats the purpose.
Hmm, interesting idea. I'm not very familiar with JSDoc so don't know how much feature parity it has to TypeScript. But given the extensive work to support JSDoc already in CoffeeScript, it's quite plausible that this could be done... in some ways, this might make type annotation easier (no hoisting, though I already did it, so not easier than curriously). But I'm not sure about the other features.
The filename extension issue could also be addressed by my previously proposed (but still hypothetical) ctsc stand-alone tool (and corresponding VSCode plugin) that does the necessary mangling to run coffee + tsc, for type checking. I think for actual building to JS many people use Babel to remove types (without checking), and I'm guessing this could be done with an appropriate --transpile option. So maybe you never/rarely need to actually generate .ts files (except that ctsc probably does so as an intermediate step).
A small update: I implemented object types. It's particularly fun to be able to use CS indentation-based notation to write these:
object ~
key: string
value?: any
object =
key: 'one'
value: 1
object =
key: 'none'This translates to the following TypeScript (which tsc confirms has no errors):
var object: {key: string, value?: any};
object;
object = {
key: 'one',
value: 1
};
object = {
key: 'none'
};I wondered about using ~ or := within the object types, like object ~ {key ~ string, value ?~ any} but there are disadvantages to that approach (e.g. it doesn't gel well with CS's existing indentation-based object parsing), and I don't think it fits TypeScript's pattern for types that mimic the object (e.g. function types specify the return value with -> not :).
I'm planning to keep the original post up-to-date with a list of features so that it's easier to track. I started a wiki page to list features, and features left to add, on my branch, so that it's easy to track. (Happy to move this somewhere else/official.)
You all are doing the lord's work. I'll throw out that it would be nice if there were an option to output AssemblyScript as well: https://www.assemblyscript.org/. There is a good bit of WASM based blockchain stuff coming down the pipe and it would be awesome to have a clear, readable language to build wasm without having to mess with c++ or rust. It looks like AssemblyScript is a strict subset of Typescript, so I'm hoping it 'just works', but there may be some transpiring that breaks things. Here are some of the quirks: https://www.assemblyscript.org/basics.html#quirks
@skilesare Thanks, I wasn't familiar with AssemblyScript. That's certainly a stretch goal, but I agree that it'd be nice if it'd be easy for a user to stay within the subset of TypeScript that it offers. Their intro example looks fairly easy... but e.g. CS converts == to === and given the quirks, you'd want to change that back to ==. Oh, a more significant problem is that closures aren't supported, and CS generates those itself when using statements as expressions. But there should still be a subset of CS that works OK.
This seems like one argument in favor of outputting TypeScript instead of JSDoc. More generally, the extensive tooling around TypeScript (e.g. perhaps also Deno of #5150) are probably further arguments for TypeScript output — while tsc might support JSDoc, some other tools presumably do not. I think the existence of Babel's TypeScript plugin removes most of the advantages of JSDoc output (though of course it could still be nice as an option). So it seems like TypeScript output would be the first priority? We should probably investigate how hard it would be to get either form supported in VSCode, though, as that's a top priority of TypeScript tooling.
This seems like one argument in favor of outputting TypeScript instead of JSDoc
Outputting TypeScript is also likely far less work, and includes more information than JSDoc. There are things you can express in TypeScript that aren’t supported in JSDoc.
I lean in the same direction as Jeremy. To me TypeScript and CoffeeScript are contradictory and must not be merged.
I get that one might want or need to have type information available to cleanly import and make use of code written in CoffeeScript in TypeScript. I believe that this can already be accomplished with flow/jsdoc comments, smart IDEs, or manually written .d.ts files and does not demand language-level support.
It's not just that a CoffeeScript developer needs to be familiar with the syntax to a certain extent, they might also have to read/understand/update code written with said new syntax, rather than just glimpse at it.
The added noise of above examples/WIP syntax makes it, at least for me, much harder to parse the code with a pair of eyes.
I believe that a big part of CoffeeScripts beauty is contributed by it's scarse use and a very limited set of special characters.
I expect that once people/projects start over-using type declarations (you know they will), we'll all have to deal with code that's hard to read and thus hard to maintain.
Extensive typing support (on a language-level) will no longer be used for the sake of compatibility but because it's there and because it's what non-CoffeeScript users want/demand.
And heck, at that point I'd personally rather use TypeScript than have to deal with what could have been CoffeeScript.
Regarding CoffeeScript at the workplace:
There's way more TypeScript users available than there are CoffeeScript users and that's not gonna change.
And for that very reason, rationally thinking project leads will continue to choose TypeScript over CoffeeScript.
@Inve1951 Thanks for your input! Adding types to CoffeeScript is certainly not for everyone, and that's why it's optional. You could make the same argument for JSX: if you really want to use React, why not just switch to the official JSX language? But I take it from your recent bug report that you use CoffeeScript's JSX support. (As an aside, JSX is so much nicer when if and for expressions return values, as they do in CoffeeScript, so you don't need to use the much uglier && and .map syntax.) Both JSX and the intended typing support are essentially passthroughs to enable CoffeeScript to be used in more contexts; in my opinion, they don't mess with the language and its beauty.
I don't quite follow your argument, so if you don't mind, I'd like to challenge a few points:
I get that one might want or need to have type information available to cleanly import and make use of code written in CoffeeScript in TypeScript.
Type checking also has a significant advantage to someone writing CoffeeScript code, or for CoffeeScript code that uses TypeScript code. I don't want to stop writing in CoffeeScript, but I also want the extra bug checking that type checking affords; I routinely find and fix bugs that would have been detected by a type checker, so typing would save me time. I believe there are many others in this boat, though it would be interesting to do a survey.
I believe that this can already be accomplished with flow/jsdoc comments ...
Are you claiming that JSDoc comments such as
add = (a ###: number###, b ###: number###) ###: number### -> a + bare easier to read than the proposed syntax
add = (a ~ number, b ~ number) ~ number -> a + b? It's also worth keeping in mind that an example like the above doesn't need any types, because TypeScript can often derive types automatically. So most of the time "typed" CoffeeScript would be the same as untyped:
add = (a, b) -> a + bI expect that once people/projects start over-using type declarations (you know they will), we'll all have to deal with code that's hard to read and thus hard to maintain.
People will write ugly code in any language. 🙂 I believe adding optional types to CoffeeScript enables clean typed code (much cleaner than TypeScript), just as CoffeeScript today enables writing clean untyped code. I have a harder time seeing that CoffeeScript with JSDoc is a fun way to write typed code.
Correct typing is no small feat, so I doubt there will be a proliferation of types like you suggest. I am part of a ~17,000-line open-source JavaScript project that added Flow typing a couple years back. It took months to accomplish. For small projects, there's no reason to add types; it would just slow you down. But types make large codebases much easier to maintain.
For comparison, I believe Python is generally considered to be one of the most readable programming languages, and it added optional typing support in 3.5. Most Python code doesn't use optional types, and that seems fine to me.
Extensive typing support (on a language-level) will no longer be used for the sake of compatibility but because it's there and because it's what non-CoffeeScript users want/demand.
It's also what several CoffeeScript users want. Dozens reacted to the original post, a few have posted here, and I personally know several others, but I suspect there are many more. To be clear, CoffeeScript is my primary language of development, and has been for several years. I don't write TypeScript code because it's not (well) supported by CoffeeScript.
It's not just that a CoffeeScript developer needs to be familiar with the syntax to a certain extent, they might also have to read/understand/update code written with said new syntax
I maintain a bunch of TypeScript and Flow code too, despite knowing mostly JavaScript and CoffeeScript. I don't find it hard. I only wish that the code were in a typed CoffeeScript so that the notation could be that much better. By preventing people from conveniently writing types in CoffeeScript code, you push people (such as yourself) to TypeScript, which makes it harder for CoffeeScript fans to maintain that code.
I believe that a big part of CoffeeScripts beauty is contributed by it's scarse use and a very limited set of special characters.
I agree: Python words like and, Perl words like unless, and if ... then ... else ... replacing ?: are all great choices. (Although CoffeeScript also adds some special characters, like @ and ::.) In trying to think of good typing notation, perhaps we should try to come up with a word instead of a symbol, like a synonym of is-a?
Are you claiming that JSDoc comments ... are easier to read than the proposed syntax ... ?
In fact I am. But this is primarily due to the visual separation and could of course be adapted by editors when this feature lands. The typing being faded makes it less prominent to the eye allowing you to read the rest more easily.
I am already in favor of a more concise flow comment syntax than wrapping it in 6 hashtags but I'm not yet convinced that type information should be more than comments.
Not very relevant here but since you brought it up:
I disagree with you on loops being cleaner than .map in CSX. The following is typical CSX in my projects:
render: ->
{ userIds } = @state
<div class="users">
{ userIds.map (userId) ->
<User id={userId} />
}
</div>Looking at this now I gotta say it's not very readable. So perhaps GitHub's code blocks aren't a good measurement for readability after all.
I'm glad you took my feedback with a smile and am looking forward to seeing where you guys take this.
As someone else who prefers Coffeescript syntax but sees the benefits/power of Typescript (and is willing to do some hacking on compilers), not to rain on any parades (I think any hacking/investigation of it is a good thing) but for similar reasons that I abandoned the run-ESLint-against-transpiled-JS approach in favor of eslint-plugin-coffee + a legitimate Coffeescript AST, I think the approach of emitting Typescript-compatible output from the Coffeescript compiler is fundamentally flawed because such a core part of the value of Typescript in practice is the in-editor tooling
Basically I don't see how you could get eg in-editor type hints without something very contorted like writing your own Language Server Protocol implementation which tried to map the original source to the transpiled Typescript, forward the request to its Language Server Protocol implementation (tsserver), un-map its response, and return it. Probably impossible to do reliably, lots of work, and hacky
So I started poking around the Typescript compiler. I've only taken baby steps, but there's a lot that seems to recommend the approach of using the Typescript compiler as the starting point (rather than the Coffeescript compiler) - the Typescript compiler already has its own baked-in concepts of different source language variants with different syntaxes (eg .ts vs .tsx) as well as a nice transpilation story - it structures its transpilation from source to target language as a series of transformation passes, so in theory you could just describe the transformation from typed Coffeescript to Typescript and then the existing transformations would take care of the transpilation (from Typescript) to JS. By doing it this way you should more or less get all of the existing Typescript tooling/intelligence (eg again baby steps but I'm able to see in-editor type-narrowing across a new unless statement:
Screen.Recording.2021-04-21.at.9.40.20.PM.mov
Pretty cool! This would also presumably allow for seamless hybrid Typescript/Coffee-Typescript codebases (like how now .ts and .tsx can coexist)
So then if what we'd all probably more or less picture is something that has as much of the syntactic 🌈 ✨ of Coffeescript as possible (while supporting all Typescript language features), the question becomes how hard will it be to slap that into the existing Typescript compiler frontend. From what I know, the rewriter step is pretty important to support some of the Coffeescript syntax (and that doesn't currently exist in the Typescript compiler) and the Typescript compiler uses a recursive-descent parser (LL?) rather than a grammar-generated one (LR?)
I guess I've just been planning to gingerly poke my way around the Typescript compiler codebase until I start wrapping my head around how to implement syntactic features, but if anyone else has interest that could help move things forward!
@helixbass The approach of modifying the typescript compiler for coffeescript-syntax support sounds very promising, because it would give us access to the whole TS ecosystem for free, as you describe. As a cofffeescript user, I would be happy with just coffeescript-like syntax (such as indentation instead of brackets) even if coffeescript semantics were not supported (implicit return, everything is expression, etc "controversial" semantics). Maybe this thing should not be called coffeescript at all, because for many it gives bad vibes, hindering a wider adoption. You know, a fresh start, taking just the good parts.
such a core part of the value of Typescript in practice is the in-editor tooling
💯
in theory you could just describe the transformation from typed Coffeescript to Typescript and then the existing transformations would take care of the transpilation (from Typescript) to JS
-
What would “describe the transformation” other than the CoffeeScript compiler itself, running within this phase of the TypeScript compiler? Isn’t this essentially what @edemaine was attempting? I thought the idea was to get the CoffeeScript compiler to take typed CoffeeScript as input and generated TypeScript as output, similar to how it currently outputs JSX (and lets other tools like Babel convert that to JavaScript).
-
Does the TypeScript compiler have a plugin interface for this phase, or would we need to fork the TypeScript compiler to wedge this in? (Somehow I don’t see them accepting a PR for this.)
In general this sounds like a great idea. Hooking into tsc to automatically become part of the TypeScript tooling ecosystem is very clever. It would be great to get this to work.
What would “describe the transformation” other than the CoffeeScript compiler itself, running within this phase of the TypeScript compiler?
These transformation passes inside the Typescript compiler are AST transformation passes. In my thinking for similar reasons as why eslint-plugin-coffee needs to run against a "non-JS-transformed" Coffeescript AST, comparably you'd presumably need the Typescript in-editor tooling to run against a "non-transformed" AST in order to avoid eg the wrangling scenario I mentioned above. So that leads me to picture working directly with the Typescript lexing/parsing to create a Typescript-style AST that hopefully can mostly "just work" with existing in-editor tooling (similar to how we've targeted a Babel-style AST because a lot of things "just work" against existing JS tooling), and then specify the "typed Coffeescript" -> Typescript transformation as an AST transformation within the Typescript compiler. That being said, it's somewhat fuzzy to me and it'd be interesting to see an attempt taking the approach of trying to hand off from the Coffeescript compiler to the Typescript compiler
or would we need to fork the TypeScript compiler to wedge this in?
Yes presumably, that's what I've been doing so far
Hey! I know that sample size of one is not particularly sound but I still hope that my feedback will help in some way :)
I'm a long-time user of TypeScript and a newcomer to CoffeeScript. Features like array slicing, chained comparisons, conditional assignments and block regexes are really useful and pretty – there are definitely places in my TS projects that would benefit from rewriting to CS. However, when I have to choose between sexy syntax sugar or good typing and interoperability with the rest of the TS ecosysem, I will always choose the latter. If CS and TS could work together, I wouldn't have to make this hard decision and I could use CS for the functional parts and TS for the OOP parts of my code. So, I for one would love to see type support and TS emit in CS!
As for the syntax: the ~ operator looks good and natural to my eye. On the other hand, the comment version (a ###: number ### = 4) obfuscates the code and looks like a nightmare to write repeatedly. But as @Inve1951 and @edemaine noted, it might be much more coffeescripty to use a keyword instead. The as keyword might be a good choice, since it's already used in TypeScript as a type casting operator.
count as number = 4
factorial = (n as number) as number ->
n = round(n)
return 1 if n < 1
n * factorial n - 1If the as keyword were allowed only
- between an identifier and the
=token in assignment - between an identifier and
,or)in a parameter list of a function definition - between
)and->or=>in a function definition
then the precedence should be unambiguous and more complicated types could be allowed (ie. longer than a single identifier).
I also have a proposal for the declaration of generic functions which feels coffeescripty enough to me:
# unconstrained type parameters T and R
map = (arr as T[], callback as T => R) as R[] where T, R ->
arr.map fn
# constrained type parameter T ⊆ User
logout = (u as T) as void where T extends User ->
u.session.terminate()I love "as" because it is so readable and readability is a key feature of coffee-script. My brain just doesn't process ":" in typescript very well and it is just so hard to process what is going on. I'd guess the hard part would be that it would break existing code that used "as" as a variable/token/function name.
I'd guess the hard part would be that it would break existing code that used "as" as a variable/token/function name.
I don't know how viable it is to implement this, but if as could be used as a keyword in one context and as an identifier in another context, the amount of breaking changes would be really small. This is how the compiler currently interprets the syntax:
n as number = 4
`count(as(number = 4))`
# possible in real-world code, but oddly specific
fib = (n) as number -> ...
`n(as(number(function() { ... })))`
# valid, but basically unreadable by a human
fib = () as number -> ... # syntax error
fib = (n, m) as number -> ... # syntax error
fib = (n as number) -> ... # syntax error
fib = (n as number) as number -> ... # syntax errorOnly the first two cases are valid code now, and I doubt that they are used in any real-world codebase – and even if they are, it would be very easy to change the old code to work again, just by adding a few parens:
n as(number = 4) # here, as is not treated as a keyword because it is not between an identifer and =
fib = n as number(-> ...) # this is even much more readable than beforeThis would allow any codebase, even if it uses as as an identifier, to migrate to the “types-enabled” version of CoffeeScript
effortlessly. Also, an as identifier could be marked as deprecated and show a (supressable) warning during the compilation.
Nice development on as for CS type declaration syntax! I was worried about this conflicting with TypeScript's notion of as, which is a cast, but it's not normally allowed in left-hand sides or arguments, so this seems like a pretty natural extension. It does lead to a bit of "weirdness" like:
a as number = b as number # defines a's type, but casts b's type without defining itThat said, the more I worked on my branch, the more I was convinced that supporting a ~ number = b ~ number (type declarations on right-hand sides or arbitrary expressions) was probably a bad idea, because it would make a ~ number | string ambiguous as either a ~ (number | string) or (a ~ number) | string. (In fact, I got stuck dealing with related grammar ambiguities. Maybe I should go back and try with as...)
Note that as is already used as a keyword in CoffeeScript in some contexts (modern import). On the compiler side (lexer/preprocessor), it might be possible to preserve use of as as an identifier in all but implicit function calls... But given the (even small) incompatibility, we'd need to jump to CoffeeScript 3 or fork. (FWIW, I'm still not a fan of forking because of the difficulty keeping up-to-date with CoffeeScript (given the extensive modifications).)
@helixbass If your approach is modifying the TypeScript compiler, why not just include the entire CS compiler (possibly emitting a parse tree instead of JS) instead of rewriting parts from scratch? Is it an issue of maintaining back references for autocompletion?
@helixbass If your approach is modifying the TypeScript compiler, why not just include the entire CS compiler (possibly emitting a parse tree instead of JS) instead of rewriting parts from scratch? Is it an issue of maintaining back references for autocompletion?
@edemaine you could certainly try that approach (of I guess roughly "hijacking" the normal Typescript lexing/parsing to delegate that to the Coffeescript compiler and then eg returning a Typescript-compatible AST), my instinct is just that it'll be more fruitful to use the Typescript compiler's own "machinery"
a as number = b as number
I love "as" because it is so readable and readability is a key feature of coffee-script. My brain just doesn't process ":" in typescript very well and it is just so hard to process what is going on
Fwiw I like the thought to try and find a more readable type-annotation syntax than :. I think part of why : type annotations are hard to read in Typescript is because : is so overloaded (eg also object key/value syntax). Whether by design or not, Coffeescript does a great job of not overloading the meaning of syntax (eg curly braces, :)
But by that same logic (assuming that a as number = b as number transpiles to a: number = b as number), you're now overloading as with two different type-related meanings, which I as expected find confusing. Also for people familiar with Typescript having eg as number (sometimes) mean something other than what it means in Typescript is a questionable choice imo
The particular symbol can change at any time before we ship this, so I don’t find it terribly useful to bikeshed the various options (~, as, :=, etc.). Just know that it’s highly unlikely that breaking changes will be allowed, nor is a version bump to 3 likely, so either the type annotations can be added in an way that doesn’t break existing syntax or some other indicator like a flag or .tcoffee extension can opt in to the new mode.
If we are going with a tcoffee code then I'd say 'as' is by and far the most readable and agreeable option. ~ has always meant 'kinda like' to my brain. 'as' implies both equality and intent. Just my 0.02 as USD.
I just want to let the team know that I'm super excited about this and IMO this will boost a lot the usage of coffeescript! I would love to know when this is released.
I'm curious, is there any status update on this? what are the next steps?
I just finished implementing a VSCode IntelliSense extension / LSP implementation for CoffeeScript based on its JavaScript compilation output, as I keep writing a lot of CoffeeScript and miss good tooling. It supports most helping tools (type check, autocomplete etc.) except source code altering actions. You can find it here GitHub / VSCode.
It does exactly what @helixbass wrote:
Basically I don't see how you could get eg in-editor type hints without something very contorted like writing your own Language Server Protocol implementation which tried to map the original source to the transpiled Typescript, forward the request to its Language Server Protocol implementation (tsserver), un-map its response, and return it. Probably impossible to do reliably, lots of work, and hacky
It is contorted and hacky, but it's just a few lines of code. Looking forward to a better replacement though, of course.
This is partly Re:
This won't give you the VS Code IDE goodness of TS while working with CS (that would be super nice, I really really want that a lot)
I have already used it for a while and it works surprisingly well.
That being said, this discussion kind of misses the goal of this issue. Regardless of our tooling, JSDoc+@tscheck can get you pretty far. And while I agree that leading block comments or inline ###* @type {string} ### param is clunkier then param := string, I am personally not super convinced that this outweighs the problems of adding new syntax (discussed above). Even this thread is mostly about better editor tooling, but we don't need TS output for that. JSDoc even has types and interfaces:
#
###*
# @typedef {{
# expiry_date?: Date
# }} IYoghurt
###
#
###* @type {IYoghurt} ###
some_yoghurt =
expiry_date: 123 # errorAlso relevant: issue about code block position
So, in conclusion, I don't think TS output would bring a lot of new things to the table, it just changes the syntax of its available features. Still, if we implement it, we'd all be using it for sure. But maybe we should assess more precisely what is already doable and what is not?
I've been using CoffeSense for a bit now and I think it really is working pretty well. And I figured to type my own CS code I will probably just write d.ts files. That does the job and doesn't clutter up my code with information I can get through intellisense now.
I'm late to the party, but I want to share my thought on this as well.
I like CoffeeScript a lot, but never had the opportunity to create any sizable project with it, and I would not. The lack of typing makes it unusable in any big project, at least for me, and I think that for a lot of devs too, so this is pretty exciting, and it would attract some attention back to CS.
As for the syntax, I pretty much agree with the use of as keyword for typing definition and assertion, := looks too much like an attribution.
One thing to note is that CS should not differentiate interface from type, as it would add an unwanted complexity, instead try to default to interface when is a simple object type.
# not an interface
type Meter = number
# interface
type Animal
move: (meters as number) -> void
# interface
type Dog extends Animal
bark: () -> void
# extends instead of implements to avoid adding unnecessary keywords
# I'm not sure if this would be good or not
class Corgi extends Dog
name as string
constructor: (@name) ->
bark: () ->
console.log "#{@name} says: Au Au"For generics, It would be lovely, but I think it would be very, very hard to implement, to use a Python-like approach, treating generics as type aliases.
generic T
generic TArray extends unknown[]
# another approach is to use the type keyword for consistency
type T = generic
type TArray = generic unknown[]
type Mapper = (item as TArray[number]) -> T
# would be compiled to
# type Mapper<T, TArray extends unknown[]> = (item: TArray[number]) => T
map = (list as TArray, fn as Mapper<TArray, T>) as T[] ->
# would be compiled to
# function<T, TArray extends unknown[]>(list: TArray, fn : Mapper<TArray, T>): T[] {I'd love to help with the implementation, but sadly I have neither the time nor the technical knowledge to.
Fortunately for the possibility of adding TypeScript support without incurring breaking changes, there is a secret stash of reserved keywords, some of which CoffeeScript has never used:
Lines 1255 to 1262 in f9c3316
Notable in this list is interface and enum, two of the biggest features of TypeScript. We could simply create new grammar for these keywords, allowing them to declare blocks equivalent to the same in TypeScript. Doing so wouldn’t be a breaking change, since these keywords currently always error.
The other “big” keyword in TypeScript is type, but there’s a lot of debate around when it should be used; see https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces and https://stackoverflow.com/a/65948871/223225 in particular. There seems to be consensus that interface should be the default choice for when either is an option, and there are very few cases where type is the only option, so in practice this means that most of the time interface should be chosen. This is good for us, in that type isn’t one of our reserved keywords and therefore we can’t add it without a breaking change (or without doing something clever, like making its use only allowed within something else currently disallowed, such as inside an interface).
We could even do something very CoffeeScripty and improve upon TypeScript by having interface support all the features of both interface and type. When code is written that is only achievable via type, like interface Fruit = 'apple' | 'orange', it would be output as type Fruit = 'apple' | 'orange'; but otherwise output as the default interface. This is in the spirit of how CoffeeScript collapses variable declaration and assignment so that you never have to think about the distinction between the two; in strongly typed CoffeeScript, you’d never need to consider the difference between interface and type. Everything is an interface.
Obviously there’s still a lot else that we’d need to figure out besides just these keywords, but I thought I’d throw this out there as a potential solution to one of the many problems we’d need to solve to support at least a meaningful portion of TypeScript’s features.
Very cool! (Has interface been there since v1? Maybe there was the idea of Java-style interfaces that never materialized.)
Anyway, I like the idea of collapsing interface and type, assuming that's technically feasible. (It seems consistent with the choice to lack const annotations, as types seem partly like const.)
Speaking of technically feasible, I was thinking lately about whether : could actually be used for type annotation in a backward-compatible way. (Partly inspired by looking at the Python type-hint grammar lately.)
- Function annotation is already not ambiguous, because you can't have an object literal in an argument or after an argument list:
(x: number) -> xcan't currently parse(x): number -> xcan't currently parse
- The hard case is with variable typing. I see a few options:
var x: numberorlet x: number(to use some of the available keywords). I'm not sure we want to go back down thevar/let/constrabbit hole though. (As an aside, I still thinkfor letis a good idea; maybe I'll put together a PR sometime for consideration. It would reduce the reliance ondofor making closures.)x: number = 5(require initialization when typing). Sadly, this has an existing meaning, though it took me a while to realize:{x: (number = 5)}.(x): numbercan't currently parse. Not very pretty though.
I still like the ~ and as alternative options, but they're not backward compatible (though I still feel ~ is close). I have another reason not to use :=: in Python 3.8, it has the same meaning as JS/CS =. This would be pretty confusing I think, given how close the CS and Python syntaxes are.
One more idea, inspired by the keyword list: We could use x implements number as a declaration of type. Verbose, but it matches interface for type declaration.
Whatever we decide for declaration, I wonder how to support TypeScript's as casting operator. One option would be: if as is assigned anywhere in the file, then treat it as usual (as foo -> as(foo)). But if as is never assigned/imported, treat as as a keyword. This would be slightly backward incompatible in the case that as is a function defined in the global scope (not in this file), and a program wanted to call that function in a chain like x(as(number)) (the meaning of x as number). But maybe that's rare enough?
I am hoping to finally return to working on my branch, to see if I can do better this time at resolving grammar ambiguities. Luckily this can be done before we figure out what notation we want to use.
I also agree := is not the greatest for typing. It's used for attribution in many languages, like Python, Pascal, GoLang, GDScript... I personally like the use of implements as as type casting, it is unambiguous, easy to read and matches the class syntax. It is verbose, but type casting is rare enough that this shouldn't be a problem.
One wild idea for variable typing: use whatever type casting operator we're using, and do not do variable typing at all, so foo = bar as int, foo = bar ~ int, foo = bar :: int, foo = bar implementes int (or whatever we land on) would be declared in the compiled code as var foo: int;. This would help maintaining CoffeeScript's idea of hiding variable declaration.
As for a more compact approach, would it be impractical to use the :: syntax that TypedCoffeescript tried to implement?
would it be impractical to use the
::syntax that TypedCoffeescript tried to implement?
Alas, :: already has a meaning in CoffeeScript. n :: Int = 3 compiles to n.prototype.Int = 3. (This is notation inherited from C++, I believe.) I assumed TypedCoffeescript decided this compatibility wasn't important enough, and re-assigned its meaning, but it's something CoffeeScript (2 at least) can't do.
nth weird idea: x is instanceof number doesn't currently compile, and looks like a type spec, either type declaration or casting. 🙂
foo = bar implements int(or whatever we land on) would be declared in the compiled code asvar foo: int;
Could you explain how this would work? Are you parsing as (foo = bar) implements int? Or did you mean foo implements int = bar? (This is the notation used by most examples above.)
I do think it's instructive to examine what Python did with type hints, as Python is also a language that tries to avoid variable declarations. See this cheatsheet. Python type hints are not as nice/powerful as TypeScript's types, but the notation is relevant. (Aside from the use of :; they can use that because foo: bar didn't have a meaning before.) It's actually turned out to be surprisingly useful/interesting to type class member variables, enabling neat metaclass features like data classes. (I don't think that would be possible here, though, as TypeScript doesn't have this kind of introspection.)
Could you explain how this would work? Are you parsing as
(foo = bar) implements int? Or did you meanfoo implements int = bar? (This is the notation used by most examples above.)
No, I mean foo = (bar implements int) as it is already possible in Typescript. Coffeescript's job would be to catch the type and add it to the variable declaration, since CoffeScript declares variables at the top of the function. That'd be similar to how CS handles async and function*
As for variables without any type casting, let TS infer its type, it is already kind of good at doing that.
That leaves open space for ambiguity, if the variable is assigned to two contradicting types. In this case, an error could be raised stating that the types couldn't be resolved, or, if we want to reserve all the type checks for TS, assign to the first type, and let TS raise its own errors for that (TS check if type castings types are compatible).
At a plus, it would leave space for using : for function typing, since, as it was already noted, it's not a braking change.
Oh, cool! I didn't realize that let x = y as Type declares x's type in TypeScript. (I verified that, even if x is assigned another value later on, it retains the type set by the initializer in the let.) That does make x = y implements Type (or in many cases, x = y) a natural way to declare x's type in CoffeeScript.
This approach is actually related to recent discussion in another thread in #5377 about using let/const instead of var, and pushing let/const as far down/in as possible (often, where the variable is first declared). In those cases, we should be able to get typing of variables for free, which seems pretty nice (and, as you say, use colon types for function declarations). It might feel a little brittle (moving an assignment into a loop will prevent typing), but it's perhaps par for the course with implicit type declarations; one could always add x = 0 or x = 0 as number at the top of a function to explicitly type x.
moving an assignment into a loop will prevent typing
It was not. TS is capable of inferring that as well.
Sure, TS can always infer types. But what's special about this setup is that the let/const initializer serves as an explicit declaration of the type. Compare this TypeScript code:
let x = 5; // explicit declaration of x as number
x = 'hello'; // error(plausibly what we can compile from x = 5; x = 'hello') vs.
let x;
for (...) {
x = 5;
x = 'hello'; // no error
}(plausibly what we could compile from for ... then x = 5; x = 'hello')
The former explicitly types x, while the latter does not (but yes, TS will determine implicit type bindings -- it'll do that without any changes on our part -- and in this example is will use any). I think this is OK.
We could still use x implements number as shorthand (compared to x = 0 implements number) for declaring the type without an initializer, compiling to let x: number in the right spot.
Oh, I didn't thought of that case. Using let, but still inferring type could be done creating an auxiliary variable for first assigned block.
for ...
x = 5
x = 'hello'
console.log xlet x;
for (...) {
let $x = 5; // generated
$x = 'hello'; // error
x = $x;
}
console.log(x)Of course, that'd only be feasible if #5377, pushing variable declaration to as close to its first assignment as possible, were to be adopted.
Following this idea of type in type inference, I've discovered Hegel, which looks very promising. It's more sound, better in inference and have an easier syntax to work with, but still compatible with TS. Sadly, I've tried it, and it's early, so it's very buggy and somewhat incomplete. I see this TS integration as a great way to attract hype back to CS, so this is a no no for me.
So how would source mapping work for this? Can we source map Typescript errors and compiled code back to CoffeeScript?
CS --CS--> JS + source map --TS--> TS + source map, so only a source map adjustment is needed if TS doesn't already do that.
I'd like to weigh in on this discussion briefly and add my 2 cents.
As a developer who started on the mainframe in the 80's with Cobol and Cics and has been a self-confessed fan of Python since the 90's, I have loved CoffeeScript from the beginning.
Historically, I grew up without IntelliSense, code completion and similar luxuries, and was used to compiling my code only when I was really sure it would fit.
Thus, languages with dynamic typing never caused me any problems, on the contrary, I still consider them a great achievement in development.
Today, however, the requirements become more and more complex, the projects larger, the code base more and more extensive and thus more confusing. Only a really good IDE offers support here. And here we are at the point that is crucial in my opinion.
If you search for Coffeescript on the web, you will find, besides resources, tips and the CoffeeScript webpage itself, many voices saying that Coffeescript is effectively dead. ES6 has taken over the best features, for Coffeescript itself the time is over.
I don't see it that way. Coffeescript as a language has an aesthetic and beauty that is second to none. Furthermore it manages to implement these aesthetics in an unexpected way into the actually very ugly language Javascript as good as possible.
Typescript, on the other hand, has managed to make Javascript even uglier than it already was, but Typescript has two distinct advantages: It is fully supported by all major IDE's besides Javascript, and not only that, components all around with the language itself are supported in those IDE's. But let's stay with pure Javascript for a moment: Let's take JsDoc in conjunction with Javascript, for example. In Webstorm and VSCode you can enjoy full JsDoc support concerning the pure documentation but of course also the type descriptions within the JsDoc annotations. If you throw a tsconfig.json file into the project, I can rely on the IDE to warn me about wrong type assignments.
Also in Cofffescript I can use JsDoc, I can also use Flow, only; The IDE's don't support this for Coffeescript. I don't get any warnings, I don't get any type hints.
When I look at JetBrains YouTrack page and search for Coffeescript, I do find many calls for support for Flow or JSDoc through the CoffeeScript plugin, but JetBrains says they will not implement this and closes the ticket. And this has probably its reason also in the fact that the voices are increasing, CS is dead anyway.
So my request number 1: Keep the repository alive. Commit regularly. No one is going to catch up something with a language that was last committed to Github two years ago.
In my eyes, both Jeremy and Goeffrey are right. Big projects and big companies no longer allow projects in languages that don't at least support typehints. (And Coffescript's support for Flow in the form of comment blocks is not a feature here, they are just ugly comment blocks that are not supported by any IDE. No more, no less).
For many projects it is not enough to work with VIM, Emacs, Sublime or similar, for which you can write your own plugin. I need JIRA / Bitbucket support, must be able to edit tickets from within the IDE and link them to commits for example. So I need an IDE which is state of the art. And many developers have to use the standards used in their company. So I also expect my IDE to fully support the language I use as well.
On the other hand, I agree with Jeremy, CoffeeScript is not comparable to Typescript, it rather marks the other end of the issue. It stands out for its clarity, good readability and captivating aesthetics, whereas Typescript is unreadable, jagged and ugly to look at. Code is meant for humans and not for machines, otherwise we might as well start hacking zeros and ones into the machines again.
This brings me to my second request: I don't think it is necessary to change the language as such. But you as creators and maintainers of the language could possibly intervene for example with JetBrains not to stop the support for CofffeeScript but to integrate it regarding JSDOC and FLOW and the resulting typechecking as they do with Javascript itself.
It doesn't help to write another plugin for Webstorm or VSCode, you must work on it that the manufacturers of the IDE's understand that it is worthwhile to support this wonderful language just as they do it for JS and TS. Coffeescript still has some weight in the javascript world. Once it has been talked to death by all the Medium and Quora specialists, it will be hard or impossible to convince commercially oriented vendors that this task is still worthwhile. But for sure, if any new language feature would be implemented supporting types, I can imagine, that this would create a momentum in the JS world, which will not be ignored by such vendors.
As mentioned above: Just my two cents on this.
Finally, I just want to thank you for one thing: CoffeeScript. Great job! Thanks.
Just an update that I'm actively working on my typescript branch again (after a several-month hiatus). I surmounted the grammar ambiguity problems I was having back in April, and am back to adding features and squashing bugs. Current state of features (see also these tests).
There's now a Discord server where we're discussing ideas and giving more fine-grained updates. Feel free to join if you'd like to be part of this development! (chat about goals/design, collaborate on code, test it out, or just listen in) @phil294 in particular, it'd be great if you'd like to help experiment combining your CoffeeSense plugin with this (would need to use .ts extension instead of .js).
For now, I'm focusing on adding all of TypeScript's features with mostly TypeScript syntax, except for type annotation which is ~ or := instead of : (though : could still be possible). For merging into CoffeeScript 2, this will need a later review of backward compatibility (e.g. only allow as operator if it's not assigned) and/or more discussion about whether it's appropriate to add to CS. In the interim, or long term if it's decided not to fit in CS, I'm willing to make this a new language that's a fork/superset of CoffeeScript, with the intent of keeping it synchronized with CoffeeScript. Even if this eventually gets merged into CS, I think it could be good to have an interim language for people to experiment with, find bugs, find design limitations, etc. (and it may help inform whether merging into CS makes sense). Conversely, blessing it as another language might make it harder/less likely to merge into CS later on... If you have opinions about this, let me know, here or on Discord!
For now, I’m focusing on adding all of TypeScript’s features with mostly TypeScript syntax, except for type annotation which is
~or:=instead of:(though:could still be possible). For merging into CoffeeScript 2, this will need a later review of backward compatibility
I would prefer this be part of CoffeeScript proper, the way that JSX is; which means we can’t break backward compatibility. I don’t think there’s enough community support to sustain a fork, and I also think we can find workable syntaxes for everything if we try hard enough. The other advantage of keeping it in the main project is that the existing ecosystem for CoffeeScript (build plugins, etc.) would support these new syntax additions without themselves all needing forks.
I agree with those reasons, and I'm glad you're interested in merging once we get to sufficient quality. Are small breaking changes really impossible, though? Would you consider increasing the major version to 3?
While we don't need to decide this now, it would help scope the plausible syntaxes we could consider. Of course we'll aim for breaking nothing (and that might be possible), but if there's something far more convenient that would not affect most code, and old code could easily be ported via a codemod (unlike the 1 to 2 transition), would version 3 be an option?
Are small breaking changes really impossible, though? Would you consider increasing the major version to 3?
I don’t think a major version bump is practical at the moment. There aren’t enough contributors with enough time to devote to what such an effort would entail (see the CoffeeScript 2 project) and it would be very disruptive to the ecosystem. Lots of ecosystem plugins would never upgrade. At the moment I think it’s not a realistic option.
I'd like to make one more argument for ~ as the type annotation operator. Thanks to GitHub's code search technology preview, it's now possible to search for affected code across all of GitHub, which seems like a pretty big dataset. However, the search does say "Results are not exhaustive because query was too expensive to satisfy, consider refining your query!" so this may not be perfect.
Searching for /\w\s+~\s+\S/ path:*.coffee (link requires being in the preview) shows that there are no examples of the sequence identifier, whitespace, tilde, whitespace, anything. Most matches are ~s in comments or in strings. The only other examples are of the form
if ~ array.indexOf(item)or
if ... or ~ array.indexOf(item)In a simpler form of the search, I also found examples like
if !!~ array.indexOf(item)Indeed, ~array.indexOf(item) seems like common advice around the web for the use of ~ (as a quick way to check for -1). The other common use is ~~x for truncating floats.
The good news is that, in my experimental branch, all of the above examples are correctly treated as a unary operator. ~ would have to be preceded by an actual identifier (not a keyword like if or or) and a space to get treated like a binary ~.
The only hypothetical bad case is if someone wrote e.g. doSomething ~ array.indexOf(item). I maintain that this is very weird way to write this code; it would make a lot more sense as doSomething ~array.indexOf(item), and I would conjecture that very nearly all code in the wild would (and it seems this is true for all of GitHub). For comparison, doSomething - array.indexOf(item) is not an implicit function call in CoffeeScript; it's a subtraction.
So... if we're considering slight breaking cases of edge cases, perhaps it's worth reconsidering binary ~? I do believe this would break extremely close to zero real-world code.
For now, I’m focusing on adding all of TypeScript’s features with mostly TypeScript syntax, except for type annotation which is
~or:=instead of:(though:could still be possible). For merging into CoffeeScript 2, this will need a later review of backward compatibilityI would prefer this be part of CoffeeScript proper, the way that JSX is; which means we can’t break backward compatibility. I don’t think there’s enough community support to sustain a fork, and I also think we can find workable syntaxes for everything if we try hard enough. The other advantage of keeping it in the main project is that the existing ecosystem for CoffeeScript (build plugins, etc.) would support these new syntax additions without themselves all needing forks.
Do you think that it could make sense to introduce codemod supported small breaking changes if the possibility of them breaking any tooling is small? For example if we adopt ~ we might not need a codemod since as @edemaine showed this might not even exist in any existing code, but even if we did need a codemod, isn't it very unlikely that a tiny change like this would break any tooling?
Do you think that it could make sense to introduce codemod supported small breaking changes
I’m strongly averse to breaking changes for the purpose of adding a new feature. We introduce breaking changes to fix bugs, or to (sometimes) match ES output for equivalent syntax, which is arguably also a bugfix; but that’s about it. From the perspective of a user who doesn’t care about TypeScript support, having a random semver-minor bump of CoffeeScript introduce a breaking change for a feature you don’t want would be extremely frustrating.
We can do this without breaking changes. We owe that to our users. CoffeeScript is a mature project with lots of users who want to upgrade only to stay current with new ES syntax (like #5391) and they don’t want to need to run codemods or potentially pore over an old codebase because a minor bump of CoffeeScript introduced a breaking change.
I've been working on my own language that inherits many features from CoffeeScript and additionally supports a large portion of TypeScript syntax. In my language, something like x: number = 23 is valid code and outputs valid TS and JS code. However, I included many features from vanilla JS and TS because I like some of the syntax there better than CoffeeScript syntax and the user gets to decide which language they want to write in.
I haven't documented Storymatic much yet, but I'm hoping that I'll get full test coverage within 2 weeks and documentation within a month, but it'll take a while.
I noticed there was a comment saying that x: number = 23 would be breaking syntax for CoffeeScript because it's already valid and outputs ({ x: number = 23 }), but my language allows an override using parentheses to force expression-style parsing. I think it's a great idea and I hope CoffeeScript will implement better type syntax than ###: ... ###. When I saw that section on the website, I almost gagged because it seemed that CS was adding it just to check a box for static typing, rather than because the creator actually wanted static typing as a main feature. Additionally, Flow is only one type system, and TypeScript is another, more popular, type system that more people use everyday. If we're going to support any system, it should be TS, not Flow.
The TypeScript CDK for the Internet Computer has launched. It would be lovely to write typedcoffeescript for it! https://github.com/demergent-labs/azle
I noticed there was a comment saying that
x: number = 23would be breaking syntax for CoffeeScript because it's already valid and outputs({ x: number = 23 }), but my language allows an override using parentheses to force expression-style parsing.
That's a good point: (x: number) = 23 doesn't compile in CoffeeScript, so it could be used for type declaration + assignment. But we're still missing how to just declare a variable without assignment though, given that both x: number and (x: number) already have a meaning (construct an object literal). And given that those don't work, I'm not sure (x: number) = 23 is the best notation for declaration + assignment.
My current favorite notation for variable type declaration is probably let x: number / let x: number = 23. But let is a relatively large can of worms to open, so we might want to understand that first. There was an old discussion about this, and many felt declaring variables was antithetical to CoffeeScript, but in a typed CoffeeScript I think it makes sense. It's also a nice alternative to do for block-scoped variables. For example, I'd love to be able to write
for let x in list
queueMicrotask -> console.log xinstead of
for x in list
do (x) -> queueMicrotask -> console.log xStorymatic looks cool, though I find the shift from arrows to fn rather jarring.
Sorry @edemaine, I haven't updated the docs yet. I removed the fn syntax and changed to CS's arrows and bound arrow syntax as I think it looks cleaner.
In my language, I also included a rescope keyword that instills a let statement directly in whatever scope the statement is contained within. I think it's a great place to resolve ambiguities as it doesn't have any conflicts and it would be a great keyword to add to CS.
Here's how the rescope keyword works in Storymatic as an example of prior art. The first code block contains the source code and the second contains the output JavaScript (when parsing each block individually). Note that an assignment recognizes when a rescope statement is in scope and doesn't create a let declaration.
rescope a
rescope a: number
rescope a = 32
a = 56
if true
a = 78
if true
rescope a = 54
let a;
let a: number;
let a = 32;
let a = 56;
if (true) {
a = 78;
}
if (true) {
let a = 54;
}We could also have a keyword such as rescope or scope available for use in for loops. E.g.
for scope x in list
queueMicrotask -> console.log xwould compile to this:
var i, len;
for (i = 0, len = list.length; i < len; i++) {
let x = list[i];
queueMicrotask(function() {
return console.log(x);
});
}I've been working on a solution to this https://github.com/DanielXMoore/Civet
Along the lines of Jeremy's preference it is a sister project that has ~98% compatibility with existing CoffeeScript while adding support for TypeScript types and reconciling ES features.
Take a look. Your wildest dreams might just come true.
Is there ever likely to be any consensus/progress on this?
I’m digging into using Qwik for a bunch of projects going forward, which unfortunately is pretty much TypeScript (and JSX) only, currently…
Being able to compile CoffeeScript -> TS + JSX, without actually having to write that syntax (both are ugly as shit 😖) would be super sweet!
Related to QwikDev/qwik#2878.
Just an update that I'm no longer working on my TypeScript branch of CoffeeScript, and have instead shifted my efforts to Civet (including some nice JSX improvements). I'm not sure we have anyone using Civet in Qwik yet, but we'd be very happy for this to happen. (See this issue for some related discussion.)
My opinion hasn’t changed. I’m happy to add TypeScript support (or a subset of TypeScript syntax support) if we can find a way to do so without breaking changes; and someone wants to put in the effort. Perhaps some of the work in Civet could be ported over.
Another option is to improve our block comments support to allow using them for JSDoc comments that can be typechecked by tsc --noEmit. The block comments are currently placed where they are to try to enable Flow support, but a) I’m not sure if that ever got working fully, and b) I’m not sure if the placement of comments before or after particular tokens is considered a breaking change. If someone wants to put in the effort to get block comments to a state where they can support JSDoc in all the places where TypeScript expects it, that’s another way forward.
Lastly, JSDoc could be the output of some new syntax. So say there’s some syntax that someone comes up with that’s non-breaking and allows for defining the types of parameters and types and so on. That could be compiled into inline JSDoc comments that TypeScript can understand, while still preserving that CoffeeScript itself is just outputting runnable JavaScript files (with these extra comments). This is perhaps the best of all, as no extra build steps are required; tsc --noEmit could be run on the generated output as a separate check like a linter.
I will admit, I don't have time to go through the entire thread right now. But I used coffescript as my primary language on many many projects. I've read the coffeescript compiler and that taught me to design programming languages (and gave me a very thorough understanding of the language itself). As I expanded my career, I worked on microservices, devops, and many other areas of software in addition to front end development. During that time I came to see the absolute necessity of typed code. When you're working on a large project with 40+ other engineers, lack of types is a disaster. It slows down development and increases frustration by a massive amount.
I loved coffeescript. It's my favorite language syntax-wise. It's powerful, expressive, and beautiful in my opinion. Like I said I can read it natively faster than I can read the equivalent JS which is not the same for many people. I'd love to use it again.
But after 15+ years in the field, I would never consider starting a new project without typescript. It's a non-starter. Lack of types is something junior devs appreciate because it gives them the flexibility to "get something running". Senior devs who work on tiny teams with only other senior devs might like it too. You can probably coordinate enough and your projects are small enough to make it work.
But for anyone doing enterprise engineering on large projects, lack of types is a silly foundational error that your project will likely never recover from. It's a ridiculous proposition. And unfortunately that eliminates coffeescript from any consideration whatsoever.
I feel like moving in that direction is the opposite of the spirit of what CoffeeScript was trying to accomplish in the first place.
If the spirit of coffeescript is to have a language for small projects with one or two devs, and eventually die as a language than I agree. If the spirit of coffeescript is to make writing javascript in the real world simpler, more concise and readable, and more maintainable, then I believe this is exactly contrary to what coffeescript is designed for. Coffeescript should be designed for Javascript as it is actually used and like it or not, that is typescript now.
I would advocate that coffeescript should include 1st class support for typescript and not even worry about breaking changes. Make it v3. Unless coffeescript has full typescript support, you're ignoring the vast majority of real world javascript usage. I don't doubt that there are more projects using javascript than typescript. But they're likely tiny projects with a single maintainer, or abandoned. If you go by number of engineers currently working on projects or amount of code written today, you'd fine that typescript is ubiquitious.
If an ardent supporter of coffeescrpt, someone who loves the language and wrote it for years, can't even use it myself or recommend it in good conscience, who exactly is coffeescript for? It will never grow, it will only dwindle until the last maintainer decides they're going to work on something else. For coffeescript to survive it needs full typescript support. It needs to evolve with the times or it will die. And I really hope the community can decide to evolve it rather than let it die regardless of the inconvenience of breaking changes.
Nevermind, just read about Civit. Seems like that's the future.
If anyone here is curious about whether static types are really important or not, I encourage you to explore some other languages and work with larger projects / teams / organizations. At minimum you can prove to yourself I'm wrong. It can't hurt. Take care
The above comment is incredibly hostile propaganda and reminds me of the xz-utils social engineering attack:
These social engineering attacks are exploiting the sense of duty that maintainers have with their project and community in order to manipulate them. Pay attention to how interactions make you feel. Interactions that create self-doubt, feelings of inadequacy, of not doing enough for the project, etc. might be part of a social engineering attack.
It also ignores the maintainer's most recent reply just above (#5307 (comment)):
Lastly, JSDoc could be the output of some new syntax. So say there’s some syntax that someone comes up with that’s non-breaking and allows for defining the types of parameters and types and so on. That could be compiled into inline JSDoc comments that TypeScript can understand, while still preserving that CoffeeScript itself is just outputting runnable JavaScript files (with these extra comments). This is perhaps the best of all, as no extra build steps are required;
tsc --noEmitcould be run on the generated output as a separate check like a linter.
I recall here how my very first open source contribution (#3946), where @GeoffreyBooth helped me to fall in love with open source for life, managed to achieve backwards compatibility across a very surprising divergence between Mac and Linux command-line parsing (#3946 (comment)).
Part of the spirit of CoffeeScript I value is how our compiler remains so simple and thoughtfully architected that we have sufficient inertia to spend our time to divine a beautiful unifying solution that makes our future work simpler, instead of getting stuck on the hamster wheel of "move fast and break things".
Can we rise to meet both of the challenges laid down by maintainers at the top and bottom of this thread? Can we devise a non-breaking syntax to generate simple and readable code, equally ready for the browser or a build pipeline? Can we carry on our tradition of innovation, and discover a new way to honor the spirit of what CoffeeScript was trying to accomplish in the first place?
Making new syntax is working with the stuff of pure thought. It's not like a boolean satisfiability problem, mechanically mapping requirements to features, accepting approximate solutions. CoffeeScript programmers experience the language as an extension of their mind and body. What does a type taste like? When you pick up a function, how much does it weigh?
I dutifully used flow types in block comments. It was fine. But it was a workaround. And it felt like exactly what it was: writing two separate languages side-by-side in the same file. Code switching that often is exhausting!
If I'm reading it right, we have just been given a well-defined open problem. Here we have the chance to do something truly novel.
Ok, I spent some time playing around with this: https://github.com/jashkenas/coffeescript/compare/main...cosmicexplorer:jsdoc-syntax?expand=1. I'm not proposing any specific syntax right now, but instead just did some background research.
tsc compilation with source maps
The biggest annoyance I found when trying to run tsc on jsdoc files was that the typescript compiler does not apply source maps for .js inputs. It seems to have a pretty robust concept of source mapping, so this shouldn't be terribly difficult to add (probably in getSourceFileFromReferenceWorker in program.ts), but I didn't want to fork typescript in order to test this out, so I created a script tsc-map-check.coffee which will compile coffeescript source files (using the built compiler in ./lib/coffeescript), collect their source maps, then rewrite tsc diagnostics to point to the real source spans. This looks like:
> coffee ./tsc-map-check.coffee typecheck-example.coffee
typecheck-example.coffee:8:3 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
8 f("asdf")
~~~~~~
typecheck-example.coffee is a hack that generates a valid JSDoc type annotation in the generated js, which is then checked against the rest of the source file by tsc. This only works for a single top-level type declaration, for reasons discussed below.
codegen: separate var statements, etc
desired-output.coffee is not a syntax proposal right now, but a sandbox to see what changes are needed beyond lexing/parsing. However, the basic idea for a first iteration is to allow applying a type annotation to any place expression in a top-level form, which would be synthesized by the compiler into a single type annotation for the entire top-level form. My goal here is for coffeescript source files declaring top-level symbols (functions, variables, etc) to be able to generate type annotations for those symbols, which are then emitted in e.g. a .d.ts file by tsc. This example should demonstrate a few features I'd like to note:
Annotated top-level form with placeholder syntax:
f<[=> number]> = (x<[number]>) -> x + 3
# alternatively:
f<[(x: number) => number]> = (x) -> x + 3Generated js with JSDoc:
/**
* @type {(x: number) => number}
*/
var f;
/**
* @param {number} x
* @returns {number}
*/
f = function(x) {
return x + 3;
}Emitted .d.ts from the above js:
/**
* @type {(x: number) => number}
*/
declare var f: (x: number) => number;Notes on this exercise:
- Instead of generating a single
var a, b, c;at the top of each scope, we need to separate all the top-level declarations which are typed, in order to apply a JSDoc comment to each. We don't need to change anything for top-level declarations which do not opt in to type annotation with this new syntax. - It seems the implementation (the body) of
fis type-checked correctly even without the@param/@returnsJSDoc. Given that@paramisn't able to describe argument destructuring (and doesn't need to), it's probably better to just only generate a JSDoc comment for the top-levelvar fdeclaration.
Summary
Currently, this proposal is just:
- place expressions can have a type annotation added with
<[...]>(idk if this will parse, it's a placeholder) - the annotation within
<[...]>is a typescript type expression- ...mostly.
<[=> number]>is not a valid type expression, but in the above example is sufficient to generate a return type constraint. we will not be processing arbitrary typescript code regardless.
- ...mostly.
- top-level assignments will traverse for any annotated place expressions, and if any are provided, then synthesize them into a single type constraint for the entire form
- for such annotated top-level assignments, a separate
var x;block is generated with a single JSDoc@typeannotation
I am not terribly familiar with JSDoc or typescript yet, so I'll be looking further for simple extensions to this concept. While it would be more than just syntax, I was also wondering whether an exhaustive enum would be interesting to consider (especially since they may be standardized in js soon), but typescript's enum documentation is incredibly confusing to me. I will investigate whether just generating a type annotation for a top-level object (as already proposed) is enough to get any additional compiler guarantees.
Ok, I tried to make things easier, but I ended up with a completely separate syntax proposal that I've wanted from coffeescript for many years which resolves the conundrum I was having. I will create a separate issue for that, but to summarize:
use {...} for type context instead of <[...]>
I thought starting with < would spiritually align with what we did for jsx, by saying "ok all external languages are gonna be inside a <...>. But I found:
- Parsing
<is immensely fraught.<[is a valid sequence of tokens, and e.g.4 < [5] > 3parses correctly.- It looks like jsx relies upon
<Anot having whitespace? - Notably, the jsx logic causes parsing of the string
<Ato fail at the lexing stage, because we maintain lexer state to achieve this! - I am incredibly impressed by this work, btw. CJSX has always worked like a charm for me and I never realized the difficulty of integrating it. Especially with all the shift operators!
- It looks like jsx relies upon
- JSDoc's
@type {...}annotations already happen to use{...}!- Since
{...}is exactly one of a destructuring expression or a value literal (no mutual recursion possible), it seems ripe for this purpose (?).
- Since
This change looks like:
# previous, old, deprecated:
f<[(x: number) => number]> = (x) -> x
# instead:
f{(x: number) => number} = (x) -> xactually, {...} is confusing: alternatives
EDIT: just saw this post with examples of existing {...}: #4991 (comment). I think the restriction to top-level assignments (see below) mitigates this somewhat, but I think the delimiters could be much more distinct than {...}. Maybe:
f!{number} = 3- This makes
<name>!<constraint-expr> = ...a general construct (still only applicable to top-level assignments). - We could extend this to e.g.
f!const = 3, if we wanted to introduce a general mechanism for annotating top-level assignments. - EDIT: I think even if this is parsed unambiguously,
{...}is still the wrong thing to use when this has nothing to do with objects (we are interpolating an entirely separate type language here)!.f!<[number]> = 3- If we use an explicit operator like
!to initiate a constraint expression, then we can use extremely loud syntax which can be more easily scanned!
- If we use an explicit operator like
- This makes
f{!number} = 3- Seems clunky at first: why not
f{!number!} = 3? What if we wanted to support!in type expressions? - One benefit is that it allows us to unambiguously lex the
{!token, instead of confusing it at all for a "normal"{...}expression. - Note:
f{%number%} = 3seems nicer than{!number!}.
- Seems clunky at first: why not
As of now, I like f!<[number]> = 3 the most,, where:
<place-expr>!<constraint-expr> = <rvalue-expr>is the general format (only applying to top-level assignments for now)!<[<type-expr>]>is aconstraint-exprwhich applies the contents oftype-exprinto a JSDoc@typeannotation!
We could use another operator besides ! such as @, but I'm planning to propose overloading @ elsewhere, so ! seems ideal especially since it is only a unary and not a binary operator.
no synthesizing types from all place expressions: specify a complete signature at the top-level name
- It will be much easier for a variety of reasons to avoid trying to recurse into annotated place expressions for top-level assignments--that would be super slick, but I think an initial prototype should focus just on "generate JSDoc types for top-level forms", which coffeescript is currently not able to do.
- We found earlier that
@paramand@returnsprovide no additional benefit--so we only have a single@typeannotation that matters anyway.
This means:
# previous, old, deprecated:
# both f{=> number} and x{number} fail:
# - "=> number" is not a full type expression,
# - x is not a top-level assignment
f{=> number} = (x{number}) -> x
# instead:
f{(x: number) => number} = (x) -> x
# (this was already allowed, but now it is the only accepted version)This will still generate the following js output:
/**
* @type {(x: number) => number}
*/
var f;
f = function(x) {
return x;
}I realized that when we restrict the type annotation syntax to top-level assignments as above, we also assume there is no subsequent redefinition of the same name, e.g.:
# we generate a special `var f` block for this
f{string} = "asdf"
# we would have to error against this
f{number} = 3generate const for specially-annotated top-level forms
I'm not terribly familiar with let and const semantics, but I just saw #5377 and realized that this single-definition restriction we have with top-level assignments happens to correspond exactly to what let and const provide!
So essentially, I'm proposing that a top-level type annotation might be a great use case for const especially, and this restricted form (only specially annotated top-level assignments) might be a way to avoid figuring those out more generally without breaking compatibility.
Basically:
f{(x: number) => number} = (x) -> xgenerates:
/**
* @type {(x: number) => number}
*/
const f = function(x) {
return x;
}EDIT: see above comment which proposes the general constraint-expr construction:
<place-expr>!<constraint-expr> = <value-expr>, whereplace-expris currently limited to top-level name assignments.<[<type-expr>]>is one suchconstraint-exprwhich applies a JSDoc@typeannotation withtype-expr.
If we adopt that framing, then we could propose:
x! = 3becomesconst x = 3;- so
constis always applied for!
- so
x!<const> = 3becomesconst x = 3;andx!<let> = 3becomeslet x = 3;- so a
constraint-expralways uses<...>and[<type-expr>]is specifically for a type annotation x!<const[number]> = 3becomesconst x = 3;with a JSDoc@type {number}!- similar for
x!<let> = 3andx!<var> = 3
- similar for
- however, if we want to expand
!to use in arbitrary place expressions (not just top-level assignments), that meanslet/var/constaren't allowed!- so we could do:
- so a
f!<const[=> number]> = ({x!<[number]>}) -> x/**
* @type {({x}: {x: number}) => number}
*/
const f = function({x}) {
return x;
} Looking at the above, I think it's fine that f!<const[number]> is allowed in top-level assignments, but {x!<const[number]>} isn't! There is already a strong visual distinction between top-level assignments and destructuring: top-level assignments always start with a symbol and then immediately !, whereas entering a destructuring context starts with a (, [, or {: it's easy to see.
summarizing two-stage proposal
- top-level only:
<name>!<<constraint-expr>> = <value-expr>constraint-expr = (const|let|var)?[<type-expr>]?, specifying the binding type and/or a JSDoc@typecomment.- e.g.:
f!<const[(x: number) => number]> = (x) -> xx!<const> = 3
- we would say the
constraint-exprdefaults tovarif unspecified for backwards compat - this is much easier to implement!
- recursing into arbitrary place expressions:
<name>!<<constraint-expr>>- i.e. anywhere you can destructure or name (place expr), you can write e.g.
x!<[number]> - this generates equivalent js to just writing
nameto bind the place value- but (using magic), the annotations for sub-expressions would be synthesized into a top-level type constraint
- this will definitely require the top-level assignment to be declared with
!as well- so e.g.
f!<[=> number]> = (x!<[number]>) -> xworks, or evenf! = (x!<[number]>) -> x(although this would infer@type {(x: number) => any} - but
f = (x!<[number]>) -> xwould be rejected, because the top-level namefwas not declared withf!
- so e.g.
- i.e. anywhere you can destructure or name (place expr), you can write e.g.
I like the idea of f!<const[(x: number) => number]> = (x) -> x (top-level only, no recursion) a lot. It generates very unambiguous and idiomatic js output. It provides an opportunity to unambiguously declare as opposed to mutation. And most importantly, it gives CoffeeScript users precise control over how their exported API is generated (including types), as opposed to having to go outside the language to explicitly declare a const or a type annotation. (please let me know if this is wrong, but it seems very useful.)
Just a comment to say that I was so happy to see this pop up on my feed. I miss coffeescript :)
As these AIs get better and better at programming I think we get back to a place where a simpler, more elegant language could actually emerge as we can push button convert entire libraries with generated tet runners and complete coverage.
@cosmicexplorer, why not use the as operator almost alike we do on real typescript, but not only for type cast?
I believe, writing in plain english is more coffeeish.
f as (x: number) => number = (x) -> x
or
f = (x as number) as number -> x
Both should give the same result, but the second is more "delicious".
Thinking only on type annotations it is enough, but I dislike this idea. I think we need full typescript power.
With typescript output in mind, I don't know how hard it will be to recognize where is a declaration and where is a type cast, but I believe it is possible and not a problem. So, this case must be possible:
A as MyType
A = {} as MyTypeoutput:
let A: MyType;
A = {} as MyType;Well, the double meaning of the operator as can be understated by the context, however it would be better to have different operators. Unhappily the is would be perfect to typed var declaration, but it is a === alias.
If different operators are necessary, the better I can propose is to use be on declarations.
A be MyType
A = {} as MyTypeThe above comment is incredibly hostile propaganda and reminds me of the
xz-utilssocial engineering attack:
I certainly did not mean to come across as hostile. I took a strong position on the topic in the hopes that the maintainers might consider this important. I can't thank jashkenas enough for creating the language, and want to express my immense gratitude for all the hard work the other contributors have made over the years. I do not want to sound entitled or ungrateful whatsoever.
I was using coffeescript 15 years ago! That the language is still actively maintained is a remarkable accomplishment. It's my favorite language to write, and if I didn't care so much about it I wouldn't have bothered commenting.
Interactions that create self-doubt, feelings of inadequacy, of not doing enough for the project, etc. might be part of a social engineering attack.
I didn't mean to make anyone feel inadequate. Sincere apologies and thanks for your contributions. Rest assured I will not be asking for any access to the code, nor do I have plans to submit any PRs for the foreseeable future.
It also ignores the maintainer's most recent reply just above (#5307 (comment)):
Lastly, JSDoc could be the output of some new syntax. ...
Interesting idea. I don't think the JSDoc is actually typescript, and I don't think it supports everything typescript supports. It does sound like a decent way to get some static type checking, but I'd be hesitant to make this change if it will make typescript support any more difficult to implement. It might end up being too little for those who need typescript, and rarely used by people who don't think about types anyway.
Thank you for your post. I did have a longer response typed out but I think it largely rehashes points I've made before, and feels unnecessary/off-topic in this thread.
As I mentioned above, I wouldn't post this unless I cared about the language. None of this is an attack on the language, or meant to dismiss in any way all of the extremely hard work so many people have put in over the (many) years. My gratitude is immense and out of the dozen or so languages I have written regularly, it's still my favorite syntax. I truly just want to see the language remain relevant and gain in popularity, and I'm just not seeing how that happens without typescript.
I don't think the JSDoc is actually typescript, and I don't think it supports everything typescript supports
You may think that, but then you'd be rather wrong - JSDoc is very close to being feature-complete, you can do all sorts of advanced stuff like generics, type casts or type definitions. Most exceptions can be externalized into separate type definition files. Which is admittedly annoying, but well it works...
Also I want to re-emphasize that while having a dedicated, backward-compatible TS-in-CS syntax would be neat, you can instead already also output JSDoc with CoffeeScript by writing JSDoc like ###* @type {MyType} ###. The positioning of these comments can be screwed up though. Full TS integration works best with it with VSCode with CoffeeSense (disclaimer: I made it)
@brandon-fryslie: I really appreciate your heartfelt and thoughtful response here! I absolutely understand and share your passion for improving the language ^_^! I should have made it clear I was characterizing a string of comments, and yours just happened to be the most recent (I'm sorry!). I really appreciate the time and care you took to elaborate on your thinking here--I'll make sure to give you the same courtesy in the future!
I'm just not seeing how that happens without typescript.
I totally agree! Geoffrey Booth described a path towards a middle ground with JSDoc, but I don't know anything beyond that outline! I'm trying to poke at it--see my tsc wrapper above which fixes up stack traces with source maps. I think the right way to solve that is to make tsc process source maps for .js input files itself instead of hacking it into cake--I made this change in a local tsc checkout, but I have no clue how easy it is to propose changes to tsc. Do you know whether tsc is likely to accept changes to its frontend, e.g. to process source maps for .js inputs? I'm not very familiar with typescript, but I think meeting in the middle would be a really wonderful answer to this if we could manage it.
Thanks again so much for your thoughtful reply. I would love to work with you on this if we can figure out how to get typescript a little closer to CoffeeScript!
@aurium: I love the idea of as!!! I was playing around a lot with @ (see #5471), and it happens to also be unambiguous syntax-wise. The other benefit is that "it looks like Rust(/ocaml/etc)", which I think is why I was drawn to it, but even if it is technically unambiguous, I think it's almost definitely confusing and ambiguous for users!
When I was working on the branch that became #5475, I was also looking to use @= as a way to explicitly declare a variable. I think that might still be useful, as it seems orthogonal to the as proposal. But I really like as--I want to use it instead of @ for my value-level syntax proposal at #5471! Which brings me to:
Also I want to re-emphasize that while having a dedicated, backward-compatible TS-in-CS syntax would be neat, you can instead already also output JSDoc with CoffeeScript by writing JSDoc like ###* @type {MyType} ###. The positioning of these comments can be screwed up though. Full TS integration works best with it with VSCode with CoffeeSense (disclaimer: I made it)
That reminds me, I have a lot of emacs lisp code for coffeescript I haven't upstreamed yet......
But! My goal with #5475 was going to be a middle ground--no let or const yet, just allowing the user to attach comments to a var declaration with @=. #5475 ended up being way different (no syntax changes), but my next goal is to prototype @= on top of #5475 so we can reliably attach comments!
@aurium: this is one idea I had regarding @ and as in response to your very thought provoking post above:
Summary
x @= ...: distinct declaration forx- separate from other
vardecls, with comments (e.g. JSDoc) attached - not renamed (other variables may be renamed to avoid it)
- any other attempted
x @= ...in the same function scope is a compile error - must be done at function/
varscope -- not within a block (#5475 helps with this!) - still uses JSDoc for type annotations
- I personally like the idea of JSDoc as an intermediate step, because CoffeeScript already does a lot of work to craft comment output in the generated code, so this builds on that and gives us an opportunity to solve other problems, like issues with block scope or module syntax (see #5475).
- separate from other
{x: y as {z}} = {x: {z: 3}: destructure while also binding to a value withas- same as #5471, but using
asinstead of ambiguous@ - completely value-level, not type-level at all (sorry!), but I think it's MUCH better than
@!
- same as #5471, but using
@aurium @brandon-fryslie: I prototyped tsc typechecking of JSDoc output above: #5307 (comment). Do you think tsc would be open to a PR that processes source maps for .js inputs so we can avoid hacky solutions for mapping error locations like that prototype? I think I could figure it out, but assistance would be super helpful!
I think the work on the tsc side is going to be helpful whether we use JSDoc or generate our own type annotations, since either way we will be generating output with a source map. Does that make sense?
EDIT: I'm sorry @aurium, I missed where you already discussed the use of as in value context 😅 ! Was reading too fast!
I don't know how hard it will be to recognize where is a declaration and where is a type cast, but I believe it is possible and not a problem.
I totally agree! I created #5474 as a start (that's only function params), but I am about 75% confident that CoffeeScript already has a very strong differentiation between lvalue and rvalue expressions even at the grammar level, and that we can use this to introduce new syntax without breaking changes!
One other distinction I think would be good to make:
- generating type definitions for consumption by other code, vs
- enabling the use of type checking within a module/script.
JSDoc output gives us (1) without involving tsc at all. From the prototype of tsc I made above, I think it would actually also give us (2), if we can use JSDoc output within a module. The distinction is related to top-level scope--CoffeeScript actually was not aware of top-level scope until #5475, but if that is merged, then we can make the distinction between (1) and (2) (if needed)!
TODO
JSDoc output
- distinct declarations (with attached comments), e.g.
x @= ...- If we want to address (1), we can consider only allowing these statements at top-level scope.
- This gives us control over exported types!
- reach goal: syntax for
let/const- The reason this is a reach goal is because calculating variable name overlap mechanics in block scope is kind of a new problem for our poor
scope.litcoffee. - It might be simple (maybe just the same logic, but applied to block scope parents from #5475?), but this is a blocker for something like #4985 and #5445.
- I think #5475 does a really good job at making scopes less mysterious, and I would love if someone extended it to cover block scope variable overlap!
- Otherwise, I think this is lower priority.
- The reason this is a reach goal is because calculating variable name overlap mechanics in block scope is kind of a new problem for our poor
After #5475 (see the changes to scope.litcoffee), I think it's actually very easy to extend @= to arbitrary scopes, not just top-level (I wasn't sure about this before). So there's no real reason not to do that, which makes addressing (2) much more feasible!
tsc integration
- process source maps for
.js(--allowJs) inputs with JSDoc and remap stack traces- This can address (1) by generating
.d.tsfiles with--declaration --emitDeclarationOnly. - This can address (2) by checking types with
--checkJs --noEmit.
- This can address (1) by generating
See tsc prototype above -- tsc basically already does what we need. It can be made to address goals (1) and (2) separately with different command line arguments if needed.
New type syntax (?)
- new tokens and nodes for type annotations outside of comments
- This is totally possible, but it requires much more effort to avoid breaking syntax changes.
- I'm not a maintainer, but maybe this would make sense for a major version instead?
- Is there a reason comments aren't enough?
- I'm not asking this to be rude; I just thought comments for flow types worked just fine, so I'm confused as to the benefit of a new syntax and semantics for type annotations in the language itself, especially since JSDoc seems to be compatible with
tscitself. - I would be interested in thoughts about what type annotations in the language itself provides over comments.
- I'm not asking this to be rude; I just thought comments for flow types worked just fine, so I'm confused as to the benefit of a new syntax and semantics for type annotations in the language itself, especially since JSDoc seems to be compatible with
This is a restatement of above mostly, but I think there are two major workstreams I think would be incredibly useful:
- distinct decls with attached comments (e.g.
x @= ...) - fixing
tscto accept coffeescript-generated JSDoc code
And regarding the goals (1) and (2) at the top: it seems there's no point in distinguishing those on the implementation side, but it is good to keep in mind the separate use cases for types is all!
I would love to work with anyone interested on the above!
hmmmm I think I accidentally just solved the JSDoc half of this? #5477 e.g.
> coffee -c -b -s --no-header <<EOF
y = 3
###* @type {number} ###
x @= 3
EOF
var y;
y = 3;
/** @type {number} */
var x = 3;I'm going to try massaging tsc now and see how far I get, but the @= change was a +55/-0 diff after #5475 (which is larger, but also entirely internal refactoring), so I think we might be in a really good place to try out JSDoc for real?
I think I was confused: tsc has a lot of code to generate source maps, but it's not clear that it uses any of that to read source maps. Anyway, I pushed the tsc error remapping script into #5477, so you can actually play around with type checking and type definitions (.d.ts) from JSDoc comments right now!!!
Please take a look at #5477 for more context, but the key is:
x @= ...creates a distinct declaration (this is checked for you), which allows you to add comments to the declaration itself.###* ... ###comments are converted into/** ... */by CoffeeScript, so you can write JSDoc comments like this.- This works vertically as well:
###*
* @type {number}
###
x @= 3becomes:
/**
* @type {number}
*/
var x = 3;Here is an example of running the tsc script:
# e.g. run these shell commands from the repo root (you'll need to have npm installed typescript):
; cat > test-map.coffee <<EOF
y = 3
###* @type {string} ###
x @= 3
EOF
; coffee build-support/typescript-compile.coffee test-map.coffee
test-map.coffee:4:1 - error TS2322: Type 'number' is not assignable to type 'string'.
4 x @= 3
~
# it also generates adjacent .js and .js.map files:
; cat test-map.js
var y;
y = 3;
/** @type {string} */
var x = 3;
//# sourceMappingURL=test-map.js.map
; cat test-map.js.map
{
"version": 3,
"file": "test-map.js",
"sourceRoot": "/home/cosmicexplorer/tools/coffeescript",
"sources": [
"test-map.coffee"
],
"names": [],
"mappings": "AAAA,IAAA;;AAAA,CAAA,GAAI,EAAJ;;;AAGA,IAAA,CAAA,GAAK",
"sourcesContent": [
"y = 3\n\n###* @type {string} ###\nx @= 3\n"
]
}
# finally, it also generates a .d.ts type definition file:
; cat test-map.d.ts
declare var y: any;
/** @type {string} */
declare var x: string;@cosmicexplorer I think it would be great to modify TypeScript (tsc but also the API) to respect sourcemaps in the input .ts files. After some searching, this idea was in fact proposed back in 2018: microsoft/TypeScript#26843 and it hasn't been rejected yet, so I think it's fair game to develop a PR. It would benefit all present and future compile-to-TypeScript languages (possibly including Svelte, Astro, Civet, etc.), allowing them to tie more directly into TypeScript tooling without having to do manual remapping of errors. I might also be able to help, though I have limited knowledge of the TypeScript source code.