coffeescript icon indicating copy to clipboard operation
coffeescript copied to clipboard

Discussion: TypeScript Output

Open GeoffreyBooth opened this issue 4 years ago • 74 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:

  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

GeoffreyBooth avatar Feb 29 '20 00:02 GeoffreyBooth

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

jashkenas avatar Feb 29 '20 00:02 jashkenas

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.

GeoffreyBooth avatar Feb 29 '20 01:02 GeoffreyBooth

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)

xixixao avatar Mar 29 '20 06:03 xixixao

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.

GeoffreyBooth avatar Mar 29 '20 07:03 GeoffreyBooth

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

aurium avatar May 03 '20 02:05 aurium

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.

GeoffreyBooth avatar May 03 '20 19:05 GeoffreyBooth

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.

aurium avatar May 03 '20 20:05 aurium

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.

jholster avatar May 21 '20 15:05 jholster

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.

Inve1951 avatar Jul 19 '20 16:07 Inve1951

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.

GeoffreyBooth avatar Jul 19 '20 17:07 GeoffreyBooth

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)

JanMP avatar Feb 26 '21 18:02 JanMP

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.

GeoffreyBooth avatar Mar 04 '21 20:03 GeoffreyBooth

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 avatar Mar 18 '21 04:03 edemaine

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.

edemaine avatar Mar 21 '21 21:03 edemaine

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

GeoffreyBooth avatar Mar 22 '21 04:03 GeoffreyBooth

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.

jholster avatar Mar 22 '21 04:03 jholster

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

edemaine avatar Mar 22 '21 16:03 edemaine

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

GeoffreyBooth avatar Mar 22 '21 18:03 GeoffreyBooth

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 avatar Mar 22 '21 19:03 edemaine

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

edemaine avatar Mar 22 '21 22:03 edemaine

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 avatar Mar 23 '21 16:03 skilesare

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

edemaine avatar Mar 23 '21 17:03 edemaine

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.

GeoffreyBooth avatar Mar 23 '21 18:03 GeoffreyBooth

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 avatar Apr 20 '21 22:04 Inve1951

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

edemaine avatar Apr 21 '21 01:04 edemaine

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.

Inve1951 avatar Apr 21 '21 09:04 Inve1951

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:

https://user-images.githubusercontent.com/440230/115643164-40508d80-a2ea-11eb-8f5b-7adf6c5e11c9.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 :rainbow: :sparkles: 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 avatar Apr 22 '21 01:04 helixbass

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

jholster avatar Apr 22 '21 07:04 jholster

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.

GeoffreyBooth avatar Apr 29 '21 03:04 GeoffreyBooth

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

helixbass avatar May 02 '21 20:05 helixbass

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

If the as keyword were allowed only

  1. between an identifier and the = token in assignment
  2. between an identifier and , or ) in a parameter list of a function definition
  3. 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()

m93a avatar May 23 '21 21:05 m93a

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.

skilesare avatar May 24 '21 13:05 skilesare

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 error

Only 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 before

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

m93a avatar May 24 '21 18:05 m93a

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 it

That 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?

edemaine avatar May 24 '21 18:05 edemaine

@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

helixbass avatar May 25 '21 15:05 helixbass

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.

GeoffreyBooth avatar May 28 '21 04:05 GeoffreyBooth

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.

skilesare avatar May 28 '21 14:05 skilesare

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.

nschurmann avatar Jul 22 '21 23:07 nschurmann

I'm curious, is there any status update on this? what are the next steps?

nschurmann avatar Jul 26 '21 04:07 nschurmann

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)

(@JanMP, @edemaine)

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 # error

Also 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?

phil294 avatar Aug 11 '21 19:08 phil294

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.

JanMP avatar Aug 25 '21 19:08 JanMP

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.

FelipeSharkao avatar Aug 31 '21 23:08 FelipeSharkao

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:

https://github.com/jashkenas/coffeescript/blob/f9c3316aa5fed06ee539edcf31b82b9394ac4765/src/lexer.coffee#L1255-L1262

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.

GeoffreyBooth avatar Sep 20 '21 03:09 GeoffreyBooth

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) -> x can't currently parse
    • (x): number -> x can't currently parse
  • The hard case is with variable typing. I see a few options:
    • var x: number or let x: number (to use some of the available keywords). I'm not sure we want to go back down the var/let/const rabbit hole though. (As an aside, I still think for let is a good idea; maybe I'll put together a PR sometime for consideration. It would reduce the reliance on do for 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): number can'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.

edemaine avatar Sep 20 '21 03:09 edemaine

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.

FelipeSharkao avatar Sep 20 '21 11:09 FelipeSharkao

As for a more compact approach, would it be impractical to use the :: syntax that TypedCoffeescript tried to implement?

FelipeSharkao avatar Sep 20 '21 11:09 FelipeSharkao

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

edemaine avatar Sep 20 '21 14:09 edemaine

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

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.

FelipeSharkao avatar Sep 20 '21 19:09 FelipeSharkao

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.

edemaine avatar Sep 20 '21 20:09 edemaine

moving an assignment into a loop will prevent typing

It was not. TS is capable of inferring that as well.

FelipeSharkao avatar Sep 20 '21 20:09 FelipeSharkao

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.

edemaine avatar Sep 20 '21 21:09 edemaine

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

FelipeSharkao avatar Sep 20 '21 23:09 FelipeSharkao

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.

FelipeSharkao avatar Sep 30 '21 21:09 FelipeSharkao

So how would source mapping work for this? Can we source map Typescript errors and compiled code back to CoffeeScript?

FelipeSharkao avatar Sep 30 '21 21:09 FelipeSharkao

CS --CS--> JS + source map --TS--> TS + source map, so only a source map adjustment is needed if TS doesn't already do that.

CTimmerman avatar Oct 20 '21 17:10 CTimmerman

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.

robert-boulanger avatar Jan 15 '22 23:01 robert-boulanger

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!

edemaine avatar Jan 21 '22 23:01 edemaine

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.

GeoffreyBooth avatar Jan 22 '22 04:01 GeoffreyBooth

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?

edemaine avatar Jan 22 '22 14:01 edemaine

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.

GeoffreyBooth avatar Jan 22 '22 17:01 GeoffreyBooth

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.

edemaine avatar Jan 25 '22 00:01 edemaine

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.

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?

orenelbaum avatar Jan 25 '22 00:01 orenelbaum

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.

GeoffreyBooth avatar Jan 25 '22 04:01 GeoffreyBooth

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.

zsakowitz avatar May 16 '22 14:05 zsakowitz

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

skilesare avatar May 16 '22 16:05 skilesare

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.

edemaine avatar May 16 '22 17:05 edemaine

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 avatar May 16 '22 17:05 zsakowitz

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

zsakowitz avatar May 16 '22 21:05 zsakowitz