jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.49k stars 1.99k 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

edemaine commented 2 years ago

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.

FelipeSharkao commented 2 years ago

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

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

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

CTimmerman commented 2 years ago

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

robert-boulanger commented 2 years ago

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.

edemaine commented 2 years ago

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!

GeoffreyBooth commented 2 years ago

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.

edemaine commented 2 years ago

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?

GeoffreyBooth commented 2 years ago

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.

edemaine commented 2 years ago

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.

orenelbaum commented 2 years ago

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?

GeoffreyBooth commented 2 years ago

Do you think that it could make sense to introduce codemod supported small breaking changes

I’m strongly averse to breaking changes for the purpose of adding a new feature. We introduce breaking changes to fix bugs, or to (sometimes) match ES output for equivalent syntax, which is arguably also a bugfix; but that’s about it. From the perspective of a user who doesn’t care about TypeScript support, having a random semver-minor bump of CoffeeScript introduce a breaking change for a feature you don’t want would be extremely frustrating.

We can do this without breaking changes. We owe that to our users. CoffeeScript is a mature project with lots of users who want to upgrade only to stay current with new ES syntax (like #5391) and they don’t want to need to run codemods or potentially pore over an old codebase because a minor bump of CoffeeScript introduced a breaking change.

zsakowitz commented 2 years ago

I've been working on my own language that inherits many features from CoffeeScript and additionally supports a large portion of TypeScript syntax. In my language, something like x: number = 23 is valid code and outputs valid TS and JS code. However, I included many features from vanilla JS and TS because I like some of the syntax there better than CoffeeScript syntax and the user gets to decide which language they want to write in.

I haven't documented Storymatic much yet, but I'm hoping that I'll get full test coverage within 2 weeks and documentation within a month, but it'll take a while.

I noticed there was a comment saying that x: number = 23 would be breaking syntax for CoffeeScript because it's already valid and outputs ({ x: number = 23 }), but my language allows an override using parentheses to force expression-style parsing. I think it's a great idea and I hope CoffeeScript will implement better type syntax than ###: ... ###. When I saw that section on the website, I almost gagged because it seemed that CS was adding it just to check a box for static typing, rather than because the creator actually wanted static typing as a main feature. Additionally, Flow is only one type system, and TypeScript is another, more popular, type system that more people use everyday. If we're going to support any system, it should be TS, not Flow.

skilesare commented 2 years ago

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

edemaine commented 2 years ago

I noticed there was a comment saying that x: number = 23 would be breaking syntax for CoffeeScript because it's already valid and outputs ({ x: number = 23 }), but my language allows an override using parentheses to force expression-style parsing.

That's a good point: (x: number) = 23 doesn't compile in CoffeeScript, so it could be used for type declaration + assignment. But we're still missing how to just declare a variable without assignment though, given that both x: number and (x: number) already have a meaning (construct an object literal). And given that those don't work, I'm not sure (x: number) = 23 is the best notation for declaration + assignment.

My current favorite notation for variable type declaration is probably let x: number / let x: number = 23. But let is a relatively large can of worms to open, so we might want to understand that first. There was an old discussion about this, and many felt declaring variables was antithetical to CoffeeScript, but in a typed CoffeeScript I think it makes sense. It's also a nice alternative to do for block-scoped variables. For example, I'd love to be able to write

for let x in list
  queueMicrotask -> console.log x

instead of

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

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

zsakowitz commented 2 years ago

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

We could also have a keyword such as rescope or scope available for use in for loops. E.g.

for scope x in list
  queueMicrotask -> console.log x

would compile to this:

var i, len;

for (i = 0, len = list.length; i < len; i++) {
  let x = list[i];
  queueMicrotask(function() {
    return console.log(x);
  });
}
STRd6 commented 1 year ago

I've been working on a solution to this https://github.com/DanielXMoore/Civet

Along the lines of Jeremy's preference it is a sister project that has ~98% compatibility with existing CoffeeScript while adding support for TypeScript types and reconciling ES features.

Take a look. Your wildest dreams might just come true.

danielbayley commented 9 months ago

Is there ever likely to be any consensus/progress on this?

I’m digging into using Qwik for a bunch of projects going forward, which unfortunately is pretty much TypeScript (and JSX) only, currently…

Being able to compile CoffeeScript -> TS + JSX, without actually having to write that syntax (both are ugly as shit 😖) would be super sweet!

Related to https://github.com/BuilderIO/qwik/issues/2878.

edemaine commented 9 months ago

Just an update that I'm no longer working on my TypeScript branch of CoffeeScript, and have instead shifted my efforts to Civet (including some nice JSX improvements). I'm not sure we have anyone using Civet in Qwik yet, but we'd be very happy for this to happen. (See this issue for some related discussion.)

GeoffreyBooth commented 9 months ago

My opinion hasn’t changed. I’m happy to add TypeScript support (or a subset of TypeScript syntax support) if we can find a way to do so without breaking changes; and someone wants to put in the effort. Perhaps some of the work in Civet could be ported over.

Another option is to improve our block comments support to allow using them for JSDoc comments that can be typechecked by tsc --noEmit. The block comments are currently placed where they are to try to enable Flow support, but a) I’m not sure if that ever got working fully, and b) I’m not sure if the placement of comments before or after particular tokens is considered a breaking change. If someone wants to put in the effort to get block comments to a state where they can support JSDoc in all the places where TypeScript expects it, that’s another way forward.

Lastly, JSDoc could be the output of some new syntax. So say there’s some syntax that someone comes up with that’s non-breaking and allows for defining the types of parameters and types and so on. That could be compiled into inline JSDoc comments that TypeScript can understand, while still preserving that CoffeeScript itself is just outputting runnable JavaScript files (with these extra comments). This is perhaps the best of all, as no extra build steps are required; tsc --noEmit could be run on the generated output as a separate check like a linter.

brandon-fryslie commented 5 months ago

I will admit, I don't have time to go through the entire thread right now. But I used coffescript as my primary language on many many projects. I've read the coffeescript compiler and that taught me to design programming languages (and gave me a very thorough understanding of the language itself). As I expanded my career, I worked on microservices, devops, and many other areas of software in addition to front end development. During that time I came to see the absolute necessity of typed code. When you're working on a large project with 40+ other engineers, lack of types is a disaster. It slows down development and increases frustration by a massive amount.

I loved coffeescript. It's my favorite language syntax-wise. It's powerful, expressive, and beautiful in my opinion. Like I said I can read it natively faster than I can read the equivalent JS which is not the same for many people. I'd love to use it again.

But after 15+ years in the field, I would never consider starting a new project without typescript. It's a non-starter. Lack of types is something junior devs appreciate because it gives them the flexibility to "get something running". Senior devs who work on tiny teams with only other senior devs might like it too. You can probably coordinate enough and your projects are small enough to make it work.

But for anyone doing enterprise engineering on large projects, lack of types is a silly foundational error that your project will likely never recover from. It's a ridiculous proposition. And unfortunately that eliminates coffeescript from any consideration whatsoever.

I feel like moving in that direction is the opposite of the spirit of what CoffeeScript was trying to accomplish in the first place.

If the spirit of coffeescript is to have a language for small projects with one or two devs, and eventually die as a language than I agree. If the spirit of coffeescript is to make writing javascript in the real world simpler, more concise and readable, and more maintainable, then I believe this is exactly contrary to what coffeescript is designed for. Coffeescript should be designed for Javascript as it is actually used and like it or not, that is typescript now.

I would advocate that coffeescript should include 1st class support for typescript and not even worry about breaking changes. Make it v3. Unless coffeescript has full typescript support, you're ignoring the vast majority of real world javascript usage. I don't doubt that there are more projects using javascript than typescript. But they're likely tiny projects with a single maintainer, or abandoned. If you go by number of engineers currently working on projects or amount of code written today, you'd fine that typescript is ubiquitious.

If an ardent supporter of coffeescrpt, someone who loves the language and wrote it for years, can't even use it myself or recommend it in good conscience, who exactly is coffeescript for? It will never grow, it will only dwindle until the last maintainer decides they're going to work on something else. For coffeescript to survive it needs full typescript support. It needs to evolve with the times or it will die. And I really hope the community can decide to evolve it rather than let it die regardless of the inconvenience of breaking changes.

brandon-fryslie commented 5 months ago

Nevermind, just read about Civit. Seems like that's the future.

If anyone here is curious about whether static types are really important or not, I encourage you to explore some other languages and work with larger projects / teams / organizations. At minimum you can prove to yourself I'm wrong. It can't hurt. Take care