jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.46k stars 1.98k forks source link

Discussion: TypeScript Output #5307

Open GeoffreyBooth opened 4 years ago

GeoffreyBooth commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

I started a branch that adds basic type annotation support.

This is very impressive! Great work!

A few preliminary thoughts:

jholster commented 3 years ago

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 commented 3 years ago

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

@jholster Thanks also for your feedback!

GeoffreyBooth commented 3 years ago
  • 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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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!

jholster commented 3 years ago

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

GeoffreyBooth commented 3 years ago

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

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.

helixbass commented 3 years ago

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

m93a commented 3 years ago

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()
skilesare commented 3 years ago

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.

m93a commented 3 years ago

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.

edemaine commented 3 years ago

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?

helixbass commented 3 years ago

@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

GeoffreyBooth commented 3 years ago

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.

skilesare commented 3 years ago

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.

nschurmann commented 3 years ago

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 commented 2 years ago

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

phil294 commented 2 years ago

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?

JanMP commented 2 years ago

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.

FelipeSharkao commented 2 years ago

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.

GeoffreyBooth commented 2 years ago

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.

edemaine commented 2 years ago

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

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.

FelipeSharkao commented 2 years ago

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 commented 2 years ago

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

edemaine commented 2 years ago

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

FelipeSharkao commented 2 years ago

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.

edemaine commented 2 years ago

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.

FelipeSharkao commented 2 years ago

moving an assignment into a loop will prevent typing

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