Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: TypeScript Output #5307

Open
GeoffreyBooth opened this issue Feb 29, 2020 · 88 comments
Open

Discussion: TypeScript Output #5307

GeoffreyBooth opened this issue Feb 29, 2020 · 88 comments

Comments

@GeoffreyBooth
Copy link
Collaborator

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:

  1. 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.

  2. 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

@jashkenas
Copy link
Owner

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

@GeoffreyBooth
Copy link
Collaborator Author

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.

@xixixao
Copy link
Contributor

xixixao commented Mar 29, 2020

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)

@GeoffreyBooth
Copy link
Collaborator Author

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.

@aurium
Copy link
Contributor

aurium commented May 3, 2020

What about to enable compiler plugins for parsing (tokenizer, lexer) and output?
So we don't need to fork coffee when new ideas come.

@GeoffreyBooth
Copy link
Collaborator Author

GeoffreyBooth commented May 3, 2020

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.

@aurium
Copy link
Contributor

aurium commented May 3, 2020

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.

@jholster
Copy link

jholster commented May 21, 2020

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.

@Inve1951
Copy link
Contributor

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.

@GeoffreyBooth
Copy link
Collaborator Author

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.

@JanMP
Copy link

JanMP commented Feb 26, 2021

I just did some experimenting with JSDoc and coffeescript using meteor. This is what I found:

  1. Adding types to coffeescript with JSDoc is surprisingly easy.
  2. 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?
  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)

@GeoffreyBooth
Copy link
Collaborator Author

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.

@edemaine
Copy link
Contributor

edemaine commented Mar 18, 2021

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...

@edemaine
Copy link
Contributor

edemaine commented Mar 21, 2021

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 j ~ number = 5 (making type assignments assignable). Assignments during type declaration are now supported.

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.

@GeoffreyBooth
Copy link
Collaborator Author

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 the type and interface keywords, or enums, then we should explore something like .tcoffee to “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 two parser.js files 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.).

@jholster
Copy link

jholster commented Mar 22, 2021

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.

@edemaine
Copy link
Contributor

@GeoffreyBooth Thanks for the quick positive feedback! Here are some responses / further comments:

  • In testing, I found one reason we might want a .tcoffee or .tcs extension: tsc really wants the filename to be .ts, refusing to allow types in a .js file (as generated by coffee -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 =) and x := number = 5 feels 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 namely type(t = ...), but it needs to translate into the slightly different TS type 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 type either to TYPE (new) or IDENTIFIER (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, type definitions, and as operator would be enough to be interesting, though perhaps interface and/or declare should 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 new class model; so we need to take care to avoid this needlessly. On the other hand, something like s/~ */~/g (but actually dealing with quoting and such) would be a pretty simple upgrade path here.
  • TypeScript does automatic typing with var declarations, not just let and const. (I just tested that var 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] and x ~ string[]. Nothing backward-incompatible though.)

@GeoffreyBooth
Copy link
Collaborator Author

GeoffreyBooth commented Mar 22, 2021

  • In testing, I found one reason we might want a .tcoffee or .tcs extension: tsc really wants the filename to be .ts, refusing to allow types in a .js file (as generated by coffee -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.

@edemaine
Copy link
Contributor

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).

@edemaine
Copy link
Contributor

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.)

@skilesare
Copy link

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

@edemaine
Copy link
Contributor

edemaine commented Mar 23, 2021

@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.

@GeoffreyBooth
Copy link
Collaborator Author

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.

@Inve1951
Copy link
Contributor

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.

@edemaine
Copy link
Contributor

edemaine commented Apr 21, 2021

@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 + b

are 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 + b

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.

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?

@Inve1951
Copy link
Contributor

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.

@helixbass
Copy link
Collaborator

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!

@GeoffreyBooth
Copy link
Collaborator Author

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.

@zsakowitz
Copy link

zsakowitz commented May 16, 2022

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.

@skilesare
Copy link

The TypeScript CDK for the Internet Computer has launched. It would be lovely to write typedcoffeescript for it! https://github.com/demergent-labs/azle

@edemaine
Copy link
Contributor

edemaine commented May 16, 2022

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.

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 x

instead of

for x in list
  do (x) -> queueMicrotask -> console.log x

Storymatic looks cool, though I find the shift from arrows to fn rather jarring.

@zsakowitz
Copy link

zsakowitz commented May 16, 2022

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;
}

@zsakowitz
Copy link

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 x

would 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);
  });
}

@STRd6
Copy link
Contributor

STRd6 commented Dec 10, 2022

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.

@danielbayley
Copy link
Contributor

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.

@edemaine
Copy link
Contributor

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.)

@GeoffreyBooth
Copy link
Collaborator Author

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.

@brandon-fryslie
Copy link

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.

@brandon-fryslie
Copy link

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

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Oct 31, 2024

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 --noEmit could 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.

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Nov 4, 2024

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 + 3

Generated 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 f is type-checked correctly even without the @param/@returns JSDoc. Given that @param isn'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-level var f declaration.

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.
  • 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 @type annotation

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.

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Nov 4, 2024

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] > 3 parses correctly.
    • It looks like jsx relies upon <A not having whitespace?
    • Notably, the jsx logic causes parsing of the string <A to 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!
  • 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 (?).

This change looks like:

# previous, old, deprecated:
f<[(x: number) => number]> = (x) -> x

# instead:
f{(x: number) => number} = (x) -> x

actually, {...} 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!
  • 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%} = 3 seems nicer than {!number!}.

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 a constraint-expr which applies the contents of type-expr into a JSDoc @type annotation!

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 @param and @returns provide no additional benefit--so we only have a single @type annotation 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;
}

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Nov 4, 2024

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} = 3

generate 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) -> x

generates:

/**
  * @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>, where place-expr is currently limited to top-level name assignments.
  • <[<type-expr>]> is one such constraint-expr which applies a JSDoc @type annotation with type-expr.

If we adopt that framing, then we could propose:

  • x! = 3 becomes const x = 3;
    • so const is always applied for !
  • x!<const> = 3 becomes const x = 3; and x!<let> = 3 becomes let x = 3;
    • so a constraint-expr always uses <...> and [<type-expr>] is specifically for a type annotation
    • x!<const[number]> = 3 becomes const x = 3; with a JSDoc @type {number}!
      • similar for x!<let> = 3 and x!<var> = 3
    • however, if we want to expand ! to use in arbitrary place expressions (not just top-level assignments), that means let/var/const aren't allowed!
      • so we could do:
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

  1. top-level only: <name>!<<constraint-expr>> = <value-expr>
    • constraint-expr = (const|let|var)?[<type-expr>]?, specifying the binding type and/or a JSDoc @type comment.
    • e.g.:
      • f!<const[(x: number) => number]> = (x) -> x
      • x!<const> = 3
    • we would say the constraint-expr defaults to var if unspecified for backwards compat
    • this is much easier to implement!
  2. 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 name to 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]>) -> x works, or even f! = (x!<[number]>) -> x (although this would infer @type {(x: number) => any}
      • but f = (x!<[number]>) -> x would be rejected, because the top-level name f was not declared with f!

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.)

@skilesare
Copy link

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.

@aurium
Copy link
Contributor

aurium commented Nov 7, 2024

@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 MyType

output:

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 MyType

@brandon-fryslie
Copy link

The above comment is incredibly hostile propaganda and reminds me of the xz-utils social 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.

@phil294
Copy link

phil294 commented Nov 8, 2024

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)

@cosmicexplorer
Copy link
Contributor

@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:

@phil294:

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!

@cosmicexplorer
Copy link
Contributor

cosmicexplorer commented Nov 24, 2024

@aurium: this is one idea I had regarding @ and as in response to your very thought provoking post above:

Summary

  • x @= ...: distinct declaration for x
    • separate from other var decls, 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/var scope -- not within a block (refactor scopes to add top-level and block scope (related!) #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 refactor scopes to add top-level and block scope (related!) #5475).
  • {x: y as {z}} = {x: {z: 3}: destructure while also binding to a value with as

@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!

@cosmicexplorer
Copy link
Contributor

One other distinction I think would be good to make:

  1. generating type definitions for consumption by other code, vs
  2. 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

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.ts files with --declaration --emitDeclarationOnly.
    • This can address (2) by checking types with --checkJs --noEmit.

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 tsc itself.
      • I would be interested in thoughts about what type annotations in the language itself provides over comments.

This is a restatement of above mostly, but I think there are two major workstreams I think would be incredibly useful:

  1. distinct decls with attached comments (e.g. x @= ...)
  2. fixing tsc to 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!

@cosmicexplorer
Copy link
Contributor

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?

@cosmicexplorer
Copy link
Contributor

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 @= 3

becomes:

/**
 * @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;

@edemaine
Copy link
Contributor

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests