Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Feature Request] Proposal for type annotations as comments #48650

Open
5 tasks done
matthew-dean opened this issue Apr 12, 2022 · 66 comments · May be fixed by #58601
Open
5 tasks done

[Feature Request] Proposal for type annotations as comments #48650

matthew-dean opened this issue Apr 12, 2022 · 66 comments · May be fixed by #58601
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@matthew-dean
Copy link

matthew-dean commented Apr 12, 2022

Problem

  1. There are many authors that want to use TypeScript without a build step
  2. JSDoc is a verbose substitute for TypeScript and doesn't support all TS features.
  3. The above 2 problem statements are articulated in https://github.com/tc39/proposal-type-annotations, but it's unclear if that proposal will be accepted, and it would be a more limited subset of TypeScript. This could be implemented today, with a much lower lift.

Suggestion

This is not a new idea. It is a fleshed-out proposal for #9694 and is also based on the prior art of https://flow.org/en/docs/types/comments/. #9694 was marked as "Needs Proposal" so this is an attempt at that proposal. (There is also prior art / similar conclusions & asks in the issue threads of the TC39 proposal.)

🔍 Search Terms

"jsdoc alternative", "jsdoc", "flotate", "flow comments"

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Proposal

A JavaScript file would be preceded by

// @ts

The reason this is needed is to indicate the author's intent at

  1. Type-checking this JavaScript file as adhering to all of TypeScript's restraints (the TSConfig file), so a stronger version of @ts-check.
  2. Interpreting special comments as TypeScript types / blocks

Types of comment blocks:

  • /*: Foo*/ is the equivalent of : Foo (a type) in TypeScript. Other type modifiers like /*?: Foo */ are also interpreted plainly as ?: Foo
  • /*:: statement*/ is the equivalent of statement in TypeScript, and used to mark complete type / interface blocks and other types of assertions.
  • Intuitively, an author may use //: and //:: when the type / type import occupies the whole line / remainder of the line

Here's a basic example, borrowed from Flow:

// @ts

/*::
type MyAlias = {
  foo: number,
  bar: boolean,
  baz: string,
};
*/

function method(value /*: MyAlias */) /*: boolean */ {
  return value.bar;
}

method({ foo: 1, bar: true, baz: ["oops"] });

The TypeScript compiler would interpret /*: */ and /*:: */ as type annotations for TypeScript, making the entire JavaScript file a complete and valid TypeScript file, something that JSDoc does not provide.

Here are some other examples, borrowed from the TC39 proposal:

function stringsStringStrings(p1 /*: string */, p2 /*?: string */, p3 /*?: string */, p4 = "test") /*: string */ {
    // TODO
}

/*::
interface Person {
    name: string;
    age: number;
}
type CoolBool = boolean;
*/
//:: import type { Person } from "schema"

let person //: Person
// Type assertion
const point = JSON.parse(serializedPoint) //:: as ({ x: number, y: number })

// Non-nullable assertion - a little verbose, but works where JSDoc doesn't!
document.getElementById("entry")/*:: ! */.innerText = "..."

// Generics
class Box /*:: <T> */ {
    value /*: T */;
    constructor(value /*: T */) {
        this.value = value;
    }
}

// Generic invocations
add/*:: <number> */(4, 5)
new Point/*:: <bigint> */(4n, 5n)

// this parameter 
function sum(/*:: this: SomeType, */ x /*: number */, y /*: number */) {
    return x + y
}

// The above can be written in a more organized fashion like
/*::
type SumFunction = (this: SomeType, x: number, y: number) => number
*/
const sum /*: SumFunction */ = function (x, y) {
    return x + y
}

// Function overloads - the TC39 proposal (and JSDoc?) cannot support this
/*::
function foo(x: number): number
function foo(x: string): string;
*/
function foo(x /*: string | number */) /*: string | number */ {
    if (typeof x === number) {
          return x + 1
    }
    else {
        return x + "!"
    }
}

// Class and field modifiers
class Point {
    //:: public readonly
    x //: number
}

Important Note: an author should not be able to put any content in /*:: */ blocks. For example, this should be flagged as invalid:

/*::
function method(value: MyAlias): boolean {
  return value.bar;
}
*/

method({ foo: 1, bar: true, baz: ["oops"] });

Yes, the content of the /*:: */ is "valid TypeScript", but the engine should distinguish between type annotations / assertions from code that is to be available at runtime.

📃 Motivating Example

A lot of the motivations for this are the exact same as https://github.com/tc39/proposal-type-annotations; but this approach just solves it a different way, and could be done much sooner. The TypeScript engine would need to do little more than replace comment blocks in conforming .js files and then just immediately treat it as if it were a plain ol' TypeScript file.

💻 Use Cases

What do you want to use this for?
This would allow teams / individuals / myself to use TypeScript without a build step! Gone would be "compile times" while developing.

What shortcomings exist with other approaches?

  1. The TC39 proposal is more limited than this proposal, for syntax space reasons
  2. JSDoc-based type-checking is more limited than this proposal, in that it doesn't support certain types, imports / exports, and as extensive of type-checking. This would support full type-checking of JavaScript.

What shortcomings exist with this approach?

  1. This is, of course, more verbose than plain TypeScript but it is, it should be noted, much less verbose than using JSDoc for typing (and would support all of TypeScript, unlike JSDoc).
  2. There would be, of course, some tooling support that wouldn't be present at first. For example, linters would need / want to be "TypeScript-aware", to lint the code within comment blocks. And code coloring / Intellisense should work in IDEs like VSCode to treat comment blocks like plain TypeScript. But I would anticipate support coming quickly from the community.
  3. The author would need to be aware that this is really just for type annotations. That is, one could not put any runtime TypeScript in /*:: */ because that would defeat the purpose. So there may be some initial confusion around usage. See the above example.

What workarounds are you using in the meantime?
There are no current workarounds, to meet these particular goals. If you a) want to use all of TypeScript, b) don't want a build step in your JS files, there is no solution. Also, to again re-iterate the point, the TC39 proposal would also not meet these goals (like JSDoc, it also cannot support all of TypeScript), so there are benefits of doing this regardless of the outcome of that proposal.

@matthew-dean matthew-dean changed the title [Feature Request] Proposal for type annotations as comments [Feature Request] Proposal for comments as TypeScript (a JSDoc-as-TS alternative) Apr 12, 2022
@matthew-dean matthew-dean changed the title [Feature Request] Proposal for comments as TypeScript (a JSDoc-as-TS alternative) [Feature Request] Proposal for type annotations as comments Apr 12, 2022
@Jack-Works
Copy link
Contributor

Another concise comemnt suggested in that proposal is:

//:: TypeA, TypeB => ReturnType
function f(a, b) {
    return a.handle(b)
}

@lillallol
Copy link

lillallol commented Apr 12, 2022

JSDoc is a verbose substitute for TypeScript and doesn't support all TS features.

JSDoc-based type-checking is more limited than this proposal, in that it doesn't support certain types, imports / exports, and as extensive of type-checking.

We have to make crystal clear on what kind of JSDoc we are talking about here. As far as I understand from what I read from your comments/issues you are talking about JSDoc without TS.

A currently available way of enabling static type checking in .js without the need to compile, is writing all your types in .ts files and then importing them in .js files via JSDoc comments.

AFAIK the only TS feature that is not supported like this, is enum. But is that an intrinsic inability of that way of enabling static type checking?

I would happily discuss with you about anything you think is un ergonomic, verbose, or lacking features regarding this way of static type checking. In my experience it is none of that.

and it would be a more limited subset of TypeScript.

Take a look on how they are suggesting to implement generics. They will introduce breaking changes.

There would be, of course, some tooling support that wouldn't be present at first.

It is already present with what I suggest.

@matthew-dean
Copy link
Author

@Jack-Works That's interesting, and definitely //:: could be added, but I think the first step would be just essentially "escaping" valid TypeScript via comments.

Then, I think what TypeScript could add is the ability to type via function "overloading", such that this would become valid:

//:: function f(a: TypeA, b: typeB): typeA
function f(a, b) {
    return a.handle(b)
}

In other words, I think it's a much bigger ask if the existing code in comments is not valid TypeScript. Right now, in the proposal, it's basically drop-in search / replace (other than flagging certain TS as invalid in a comment, as noted).

@matthew-dean
Copy link
Author

matthew-dean commented Apr 12, 2022

@lillallol

A currently available way of enabling static type checking in .js without the need to compile, is writing all your types in .ts files and then importing them in .js files via JSDoc comments.

You're right! There are clever workarounds. But writing types inline is often more self-documenting / easier to reason about, and JSDoc still wouldn't be as concise when it comes to assigning those types to vars / params. And, I think even with those, you're still missing some things when it comes to type-checking, although I can't remember what off the type of my head. That is, I think that @ts-check still has a lighter touch for a JSDoc file than an actual .ts file, IIRC.

Take a look on how they are suggesting to implement generics. They will introduce breaking changes.

Exactly. It can't be dropped in as-is. This proposal could.

@matthew-dean
Copy link
Author

@lillallol From the TC39 proposal:

JSDoc comments are typically more verbose. On top of this, JSDoc comments only provide a subset of the feature set supported in TypeScript, in part because it's difficult to provide expressive syntax within JSDoc comments.

The motivation is the same.

@lillallol
Copy link

@matthew-dean

But writing types inline is often more self-documenting / easier to reason about

We should strive for separation of intent and implementation since it is a best practice.

But I have to admit that inline types are preferred when I create utility functions that I intend to use in multiple projects (lodash like utility functions), because like this I have to copy only a single thing from a file, and not two things (concretion and abstraction). Another use case for inline types is for defining simple types (e.g. boolean, number etc) for some variables that are declared inside functions (my projects are just functions and singletons), but again when the types get more involved I add them in .ts files.

For non lodash like projects, I usually write all the types into these files:

  • publicApi.ts : self explanatory (this files does should not depend on the private api)
  • privateApi.ts: self explanatory (this file depends from the public api)
  • sharedPrivateApi.ts : all types that are shared among the types of private api
  • testTypes.ts : self explanatory

I think you will also find that self explanatory and easy to reason about.

If you want to see the types of a concretion then you can hover over it and VSCode will show the type. If you are still not satisfied with what VSCode shows then you can ctrl+click on the imported type for VSCode to go to its definition.

JSDoc comments are typically more verbose.

This argument is not a real concern. At least in my own experience (e.g. the projects I have done). Do you really actually press more the keyboard when using JSDoc? If yes then how much? 1 key? 2 keys? You wanna get rid of those taps and introduce a new comment syntax? Is it worth for the extra fragmentation it will create? If you (and a substantial amount of people) have created projects (10k+ lines of code) with the way of static type checking I suggest, and you find these extra taps reduce DX, then fine I am with you.

Strictly speaking, when someone sticks to separation of intend and implementation, importing types via JSDoc is not necessarily more verbose when compared to writing everything in .ts files. In fact sometimes it can be less verbose.

Regarding which is more readable, I have to say that this is a matter of what someone has gotten used to. For example initially I found ts code not as readable as js code. But then I got used to it. Same for the importing types via JSDoc.

And, I think even with those, you're still missing some things when it comes to type-checking, although I can't remember what off the type of my head.

Just do not ask me how to type classes, because I have no clue about classes. Still the same question remains:

is that an intrinsic inability of that way of enabling static type checking?

or is it something that can be supported in the future?

@matthew-dean
Copy link
Author

matthew-dean commented Apr 12, 2022

@lillallol

I'm confused where you are coming from or what your argument is. In no way would what I'm (or others) proposing have a negative impact on JSDoc-style typing. If it works for you in a way that matches your development workflow, great. That's not everyone's experience, and I think it's clearly articulated by even people on the TypeScript team that the JSDoc flow is not the greatest experience, from their perspective.

So, if this doesn't match a need you have, that's okay. This isn't for you. Just like, if someone is fine with transpiling TypeScript / having a build step, this isn't for them either. But it's a clearly articulated need by other developers. This would be an alternate style of embedding types that would be compatible with the existing TypeScript ecosystem, including JSDoc-typed files.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Apr 12, 2022
@lillallol
Copy link

That's not everyone's experience

Lets be more specific with examples here.

I think it's clearly articulated by even people on the TypeScript team that the JSDoc flow is not the greatest experience

What do you mean by JSDoc flow? You mean the way I suggest? If yes then I would like to have some links, or at least if ts maintainers see that, have a discussion on that, here.

But it's a clearly articulated need by other developers.

There is already a solution for that need, which actually promotes best practices (separation of intent and implementation) rather than embracing bad practices (embracing of mixing intent with implementation). From what you suggest we end up hard coding .js files with ts. This is not done with the way I suggest : /**@type {import("some/path/without/ts/extension").IMyType}*/. That path can refer to .ts or a flow file or whatever. Like this you can make your code base work for any type system without having to change the .js or .ts files.

If your response is :

but this bad practices is already supported by ts compile to js method

then I would like to make myself crystal clear on that one: compiling ts to js as inferior way of developing to what I suggest (read here for more).

@simonbuchan
Copy link

Teeny note, there's already // @ts-check, or --checkJs. I think it's probably safe enough to just use those (who has a critical need to not change existing comments with leading :: but also run with --checkJs?)

I suspect this would be quite simple to design, add and document, especially with prior art, and few downsides (let me know if I'm wrong!)

It's not even all that much in conflict with the ECMA proposal, which can have significantly nicer experience with inline annotations.

@dfabulich
Copy link

When Flow first introduced its streamlined comment syntax, they imported it wholesale from flotate, a piece of software built pretty much entirely by one person.

Rather than a written proposal, I think what's needed here is a working prototype. I think it makes sense to experiment with Flow's syntax, and even to build on it with //: and/or //:: syntax. Try it out in a real project and see how it feels!

(I wonder whether //:: would feel better inside the function declaration, like docstrings in Python.)

function f(a, b) {
    //:: TypeA, TypeB => ReturnType
    return a.handle(b)
}

@simonbuchan
Copy link

@dfabulich one of the things I like about the Flow behavior is it's extremely straightforward: just remove the wrapping /*:: and */ to get the code Flow sees. I wouldn't want to mess with that property.

I actually took a stab already at adding it to a fork of typescript. Initially, seemed pretty easy to add to scan() in scanner.ts, but I haven't yet figured out how to get the parser to not try to parse the closing */ as an expression, so it gets very confused. I assume some .reScan*() method is kicking in and breaking everything, but maybe I also need to do skipTrivia(). Any compiler devs that are bored and have a guess, let me know!

There is an interesting issue though: the following code would seem to be valid, but give different results when run through the transpiler from directly:

let runtime = "native";
/*::
runtime = "transpiled";
*/
console.log(runtime);

Not sure if that's a bug or a feature!

@dfabulich
Copy link

Yeah, I just think it's a hassle to add four characters (/* */) for each function parameter and the return type (or maybe six if you include spaces around the comment delimiters):

function method(a /*: number */, b /*: number */, c /*: number */, ) /*: number */ {
  return a + b + c;
}
function method(a, b, c) {
  //: number, number, number => number
  return a + b + c;
}

@matthew-dean
Copy link
Author

matthew-dean commented Apr 14, 2022

@simonbuchan

Teeny note, there's already // @ts-check, or --checkJs

So there is a very low but important risk if you left this as is -- it's possible that someone is using //: or /*: in a comment start. So I think it's important to "flag" this new comment behavior. Essentially I would propose that // @ts is a "superset" of // ts-check. It's TypeScript checking plus escaping this comment syntax. In addition, you have to consider that JSDoc types and this form of type-checking could potentially be in conflict, if an author defines both, so I would propose that // @ts "ignores" JSDoc types if they are present and just treats them as comments. (Unless it's trivial to just say that this comment form would "win" if there are two type indicators.)

@dfabulich

Essentially, I feel you're not wrong (although I still disagree with this syntax as not very TS-y or JS-y); I just feel it's a huge mistake to conflate these two things in one feature, as it's a much bigger ask. These should be two different proposals.

  1. This proposal (denoting valid types in comments) -- essentially supporting currently valid TypeScript
  2. After / if proposal #1 is accepted / has feedback, is in use, adding "simplified forms" to types-in-comments. (This should be an entirely different proposal). This needs a lot more iteration and would be a much harder push because it would be potentially adding code in "escaping" comments that would not currently be valid TypeScript / JavaScript.

@matthew-dean
Copy link
Author

matthew-dean commented Apr 14, 2022

@simonbuchan As to this:

let runtime = "native";
/*::
runtime = "transpiled";
*/
console.log(runtime);

Not sure if that's a bug or a feature!

This should definitely be treated as a bug / type-checking error by TypeScript, and IMO this is a bug if Flow supports that. The resulting code is runnable but the result is not expected. So ideally, only "typing" forms would be allowed within comments.

And definitely this code should be entirely thrown out (throw an error):

/*::
let runtime = "transpiled";
*/
console.log(runtime);

(TypeScript should throw an error as runtime being undefined.)

When I have time, I can refine the proposal with specifying what is allowed / disallowed in these comment blocks, and not just escaping "any TypeScript".

@somebody1234
Copy link

somebody1234 commented Apr 14, 2022

I've been working on an implementation of this for a while:
TypeScript
TypeScript-TmLanguage
Note that you'll need to clone the vscode repository, change the url of the script that updates the typescript (and by extension, javascript) .tmLanguages, and finallly add the javascript/ folder as an extension, to get highlighting to work

Note also that it's extremely WIP and still has print debugging statements (D:) - there are still quite a number of issues, especially (or mostly?) with incremental parsing

Of note is that runtime statements (mostly) error as expected; enums and const enums are completely banned; declare should mostly error/pass as expected too however it seems to not work in some situations like this:

/*::declare const FOO: unique symbol;*/

(Forgot to mention - for TypeScript do npm run gulp watch-local and point your typescript.tsdk vscode setting to /path/to/cloned/TypeScript/built/local)

@matthew-dean
Copy link
Author

matthew-dean commented Apr 14, 2022

@somebody1234 Awesome! I still think it needs documentation of the technical details -- how / what / which things are allowed, so I want to add that to this proposal. Are there any other things you've caught in the implementation that need to be considered?

@somebody1234
Copy link

somebody1234 commented Apr 15, 2022

(Note: For reference, there's the test file I use at the bottom. It should contain all the things that are allowed)
(Note 2: For parsing, most runtime constructs are parsed in types-as-comments - the diagnostics that they are not allowed in types-as-comments are added later)

Misc notes

  • JSDoc parsing will need to be modified so they work in /*:: */ (otherwise you can't document interface members) - currently I think *\\\/ should be replaced with *\\/ (one fewer backslash) (same for any number of backslashes). Definitely very open for discussion though
    • types work fine enough when you do /** jsdoc */ /*:: type T = Foo; */
  • A general overview of how it works:
    • both the scanner and the parser keep track of whether they are currently in a type comment.
      • the scanner, via a boolean. this changes /* to be parsed as slash asterisk since it is technically already in a comment, so they cannot start another one
        • actually that will probably need to be changed to be legal again - however the boolean flag needs to stay to handle */ correctly
        • also important to note that /*: (and */ when not ending a block comment) are scanned as tokens rather than trivia (comments)
      • the parser changes contextFlags. the most important reason why it doesn't use a boolean is because contextFlags affects the flags of every node created while it is still set, which is what we want here
        • (implementation detail but) there's setInTypeCommentContextAnd which is a helper to set NodeFlags.InTypeComment if we are in a type-as-comment. if so it also attempts to parse an ending */ (which should always be after whatever is being parsed - /*::<T>*/, /*: T*/ etc)

What is allowed

  • Type annotations modified to accept both /*: T */ and /*:: : T */ (anywhere that accepts /*: */ also accepts the other, in general)
    • I believe there are seven places that have been modified - I'll add them when i remember
  • const enum and enum are disallowed completely (whether in type comments or not). currently I don't think anything else is, but if anyone knows of any other constructs that are exclusive to TypeScript please let me know
  • import { /*:: type Foo */ } (/*:: import type {} */ is allowed as `/*:: statement1; */)
  • /*:: statement1; statement2; */
  • /*:: classMember1; classMember2; */ (index signatures and declared members are allowed)
  • /*:: as T */, /*:: as const */
    • tangent, but i find it a bit noisy - personally i prefer asConst(), implemented using a variant of the wonderful Narrow<T> type
  • function foo/*::<T>*/
  • class Foo/*::<T>*/ extends Bar /*:: implements Baz */ (extends` is not allowed to be in a type comment of course)
  • function foo(/*:: this: A, */ a: B) (the comma is needed - although every case is handled individually, they are still treated as though they were transparent)
  • function foo(/*:: this: A */) (trailing comma seems to (correctly) be allowed inside the comment)
  • /*::<number>*/ 1 aka the weird type assertion
  • /*:: public */ static foo; (note that static is not allowed in types-as-comments since it affects runtime semantics)
  • /*:: declare function foo(); */ and /*:: declare const foo; */ etc. note that /*:: declare */ is not allowed.
  • /*:: function foo(); */ function foo() {}` overloads seem to parse fine (surprisingly???) but they are currently not explicitly allowed. I'll probably have to change that
  • /*::<T>*/ () => {}
  • foo/*::!*/
  • I've allowed class Foo { bar()/*::?*/ {} } but on second thought, is that even useful???
    • It seems to be legal in TypeScript, but not sure if it's a case that should be specifically handled
  • class Foo { constructor(/*:: public */ a/*: A*/) }. Thinking about it, this should definitely not be allowed. Parsing modifiers seems to be part of parseParameterWorker so it would be a diagnostic which is fine
  • probably some other things. just check below tbh - it should have everything that has been added, feel free to @ me if you have questions though
    • if you want to be extra sure - just navigate to src/compiler/parser.ts and do a search for setInTypeCommentContextAnd
function foo();
Test file
/*::
type Left = 1;
type Right = 2;
type Test = Left | Right;
type Test2 = Left | Right;
let invalid: Test;
*/
type Invalid = 1;

let annotation/: string/ = 'a';
/* normal comments should work fine /
export function fromString(s/
: string */) {
//
}

export function toString() {
//
}
let foo/*: number */;

const testNestedAsConst = {
arr: [1, 2, 3] /*:: as const /
};
const TokenType = {
CRLF: 0,
} /
:: as const /;
/
::
type TokenType = (typeof TokenType)[keyof typeof TokenType];
type Node =
| { type: typeof TokenType.CRLF; };
*/

const asConstTest = 1 /*:: as const */;

function argsTest(s/*:: : string /, s2/:: ?: string */) {
return 0;
}

function thisParamTest(/*:: this: number, / a/: string */) {
return 0;
}

function thisParamTest2(/:: this: number/ a/*: string */) {
return 0;
}

function thisParamTest3(/:: this: number /) {
return 0;
}
let foo1 /
: number /;
let testTheThing/
: number /;
// let testTheThing2/
:: : /
// let fn = fnGeneric/
::
/('ok');
class ClassIndex1 {
/::[k: string]: string;/
}
class ClassMember1 {
a/: string /
}
class ClassMember2 {
a/
: string /;
}
class ClassMember3 {
a/
:: : string /
}
class ClassMember4 {
a/
:: : string /;
}
// let a: ;
// let a /
: /;
class ClassMember5 {
a/
:: ?: string /
}
class ClassMember6 {
a/
:: ?: string /;
}
class ClassMember7 {
a/
:: !: string /
}
class ClassMember8 {
a/
:: !: string /;
}
class ClassMembers1 {
a/
: string /
b/
: string /;
c/
: string /
}
class ClassMembers2 {
/
::[k: string]: string;
/
a/*: string /;
b/
: string /
c/
: string /;
}
class ClassDeclare1 {
/
::
[k: string]: string;
declare a: '1';
declare b: '2';
c: '3';
*/
}

class ClassGet1 {
get a() { return 1; }
set a(v) {}
static get a() { return 1; }
static b = 1;
// /*:: static / a = 1;
private get c() { return 1; }
/
:: private / get d() { return 1; }
/
:: public / static set e(v/: number /) {}
}
let classGet1/
:: !: ClassGet1 */;
classGet1.d;

class ClassModifiers1 {
/*:: public /a/: number */;
}

class ClassModifiersInvalid {
/:: public static/ public a/*: number */;
}

class ClassMisc {
bar/::?/()/: number/ { return 0; }
get foo(): number { return 0; }
}

class ClassPropertyConstructor {
constructor(/*:: public /a/: number */) {
//
}
}

const what = { foo!: 1 };

/*::
export = asConstTest;

interface I {}

type Wot = {
private a: number;
// private b/: number/;
// /:: private /a/: number /;
};
declare function declareTest(): void;
/
declare function declareTestBad()/
: void
/;
/
:: declare */ let a: number;

// TODO: make sure all typescript syntax still correctly errors in js
class ClassExtendsImplements extends String /*:: implements I / {
}
class ClassImplements /
:: implements I */ {}

class ClassGeneric/::/ {
a/: T /;
b/
: [T] /;
c/
::
/() {}
d/::?/() {}
}
class ClassModifier {
/:: private/ bar/: string/
}
let letGeneric1/: ClassGeneric<1> /;
let letGeneric2/
:: !: ClassGeneric<1> /;
function fnGeneric/
::
/(it/:T/) {
return it;
}

/*::
declare module "./types_as_comments_test" {
type A = 'A';
type B = 'B';
type C = 'C';
type D = 'D';
type E = 'E';
type F = 'F';
}
*/

import { /:: type A,/ } from "./types_as_comments_test"
import { B, /:: type C,/ } from "./types_as_comments_test"
import { D, /::type E, type F / } from "./types_as_comments_test"
export { /
:: type A as A_
/ }
export { B as B_, /::type C as C_/ }
export { D as D_, /::type E as E_, type F as F_ / }
function thisParamIdk(/
:: this: Foo3 /) {}
class Foo3 {
/
::declare x: Foo3;
/
// TODO: this errors
// /::declare y: Foo3;;/
}
class Foo4 {
x/*: Foo4 */
}

fnGeneric/::<[1, String, () => {}, (a: 1, ...b: any[]) => asserts a is any, (a:number)=>a is 1, number, {[k:1]:2;}]>/();

// const instantiationExpression = [].map/::/;
const nonNullAssertion = 1/::!/;
const genericArrowFunction = /::/() => {};
const genericAsyncArrowFunction = async /::/() => {};
// note that these two are invalid
// const genericSimpleArrowFunction = /::/ foo => {};
// const genericSimpleAsyncArrowFunction = async /::/ foo => {};
const prefixTypeAssertion = /::/ 1;

/*::
declare interface AAAAA {
bar: 1;
}
*/

@Jack-Works
Copy link
Contributor

anyone knows of any other constructs that are exclusive to TypeScript please let me know

namespace / module

namespace X {
    const a = 1
}

import =

import react = require('react')

@somebody1234
Copy link

somebody1234 commented Apr 15, 2022

import = seems to be correctly handled currently - namespace/module don't though, nice catch

the fix is simple though (and it's relatively minor) so i'll hold off on committing it for now (if you want it asap though, search for Namespace (+ Module) in src/compiler/program.ts)

@somebody1234
Copy link

somebody1234 commented Apr 15, 2022

Ah... also worth noting that a bunch of diagnostic messages will be '240' expected or similar... for print debugging reasons. Those normally say "Comments as types cannot contain JavaScript".

On that note, feel free to suggest improvements to the new diagnostic messages as well

@simonbuchan
Copy link

simonbuchan commented Apr 15, 2022

Interesting approach, seems like a lot of work! I was thinking banning bad uses would be done with something like a TokenFlag that gets bubbled up to something that definitely knows if it could be emitted or not, but I didn't get very far so maybe that would be a problem.

@somebody1234
Copy link

Does seem like a lot of work - but I think it's relatively low effort compared to the alternatives. Plus this way you get a huge amount of control over where exactly they're valid, error recovery etc.
Especially compared to a simple find-and-replace. Not to mention find-and replace would replace in strings too, etc etc

Offtopic but, one concern would be, it adds quite a bit of complexity to parser logic so it might cause performance issues - however I'm guessing it's not that bad since most of the time is probably usually typecheck time anyway

@simonbuchan
Copy link

simonbuchan commented Apr 15, 2022

The profiles I've seen are all dominated by FS access, binding and type checking, but with JavaScript you can always fall off a performance cliff and have some code run 100x worse. You'd probably notice the tests being noticably slower though!

Not sure exactly what you mean by find and replace - doing that before parse would break location reporting along with all the other issues! My attempt was to get the scanner to treat /*:: and a matching */ as separate comment trivia, so the parser didn't need to pay any attention. Obviously not a complete solution, if you don't want to accept emittable code, but I felt that could be in a relatively simpler diagnostic visitor pass after parsing.

@lillallol
Copy link

only type annotation for function declarations can be imported to JS

That is not valid. You can import any type from .ts files to annotate statements.

/**
 * @type {import("./privateApi.js").IMethod} 
 */
function method(a, b) {
    /**
     * No need to use that annotation anyway since TS infers types.
     * @type {import("./privateApi.js").INumber}
     */
    let x = a + b;
    /**
     * No need to use that annotation anyway since TS infers types.
     * @type {import("./privateApi.js").INumber}
     */
    let y = a * b;
    return x / y;
}
//./privateApi.ts
export type IMethod = (a : number, b : number) => number;
export type INumber = number;

Regarding the type annotations proposal the enforcement of separation of implementation and abstractions in different files enables someone to standardise type-import annotations without the need to define a type system and simultaneously not restricting its syntax. That is exactly the proposal I want to present to tc39.

@msadeqhe
Copy link

OK. How do you annotate the type of x and y variables?

/**
 * @type {import("./privateApi.js").IMethod} 
 */
function method(a, b) {
    /**
     * No need to use that annotation anyway since TS infers types.
     * @type {import("./privateApi.js").INumber}
     */
    let x = a + b;
    /**
     * No need to use that annotation anyway since TS infers types.
     * @type {import("./privateApi.js").INumber}
     */
    let y = a * b;
    return x / y;
}

This is a simple example, but in complicated function bodies, we need a direct way to use TypeScript features within function bodies.

@lillallol
Copy link

This is a simple example, but in complicated function bodies, we need a direct way to use TypeScript features within function bodies.

Please give an example.

@lillallol
Copy link

Here is one. But then again it is a matter of support, i.e. not an intrinsic inability of the way of static typing I suggest.

@msadeqhe
Copy link

This is an example from VSCode repository:

public selectToBracket(selectBrackets: boolean): void {
	if (!this._editor.hasModel()) {
		return;
	}

	const model = this._editor.getModel();
	const newSelections: Selection[] = [];

	this._editor.getSelections().forEach(selection => {
		const position = selection.getStartPosition();
		let brackets = model.bracketPairs.matchBracket(position);

		if (!brackets) {
			brackets = model.bracketPairs.findEnclosingBrackets(position);
			if (!brackets) {
				const nextBracket = model.bracketPairs.findNextBracket(position);
				if (nextBracket && nextBracket.range) {
						brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());
				}
			}
		}

		let selectFrom: Position | null = null;
		let selectTo: Position | null = null;

		if (brackets) {
			brackets.sort(Range.compareRangesUsingStarts);
			const [open, close] = brackets;
			selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
			selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();

			if (close.containsPosition(position)) {
				// select backwards if the cursor was on the closing bracket
				const tmp = selectFrom;
				selectFrom = selectTo;
				selectTo = tmp;
			}
		}

		if (selectFrom && selectTo) {
			newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
		}
	});

	if (newSelections.length > 0) {
		this._editor.setSelections(newSelections);
		this._editor.revealRange(newSelections[0]);
	}
}

newSelection, selectFrom and selectTo have type annotations. How do you put the declaration of them to a separate .ts file? How do you import them to .js file? They are local variables.

@msadeqhe
Copy link

What about local functions and local types? If we put all of them to a .ts file, it would resemble spaghetti coding style.

@lillallol
Copy link

This is not valid .js nor .ts. Can I have minimal reproducible example? Nvm:

   /**@type {import("./some/place.js").ISelectToBracket}*/
   selectToBracket(selectBrackets) {
	if (!this._editor.hasModel()) {
		return;
	}

	const model = this._editor.getModel();
	const newSelections: Selection[] = [];

	this._editor.getSelections().forEach(selection => {
		const position = selection.getStartPosition();
		let brackets = model.bracketPairs.matchBracket(position);

		if (!brackets) {
			brackets = model.bracketPairs.findEnclosingBrackets(position);
			if (!brackets) {
				const nextBracket = model.bracketPairs.findNextBracket(position);
				if (nextBracket && nextBracket.range) {
						brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());
				}
			}
		}
                /**@type {import("./from/somewhere.js").IPostionNull}*/
		let selectFrom  = null;
                /**@type {import("./from/somewhere.js").IPostionNull}*/
		let selectTo = null;

		if (brackets) {
			brackets.sort(Range.compareRangesUsingStarts);
			const [open, close] = brackets;
			selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
			selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();

			if (close.containsPosition(position)) {
				// select backwards if the cursor was on the closing bracket
				const tmp = selectFrom;
				selectFrom = selectTo;
				selectTo = tmp;
			}
		}

		if (selectFrom && selectTo) {
			newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
		}
	});

	if (newSelections.length > 0) {
		this._editor.setSelections(newSelections);
		this._editor.revealRange(newSelections[0]);
	}
}

it would resemble spaghetti coding style.

There is no need to use a single file for your types. I usually use these:

  • publicApi.ts is used to define the public API
  • privateApi.ts is used to define the private API
  • types.ts is used to define types used by the abstractions and
    implementations of the private API
  • testApi.ts is used to define types that are used only in test files

I never had a problem with spaghetti code.

@trusktr
Copy link
Contributor

trusktr commented Sep 22, 2023

@lillallol Here's a simple repo with the first commit written in TypeScript, npm i && npm start installs typescript, builds to JS, opens a browser tab, logs to console.

https://github.com/trusktr/buildless-typescript

What's the best way (in your opinion) to convert it to buildless (keep only the static server) while keeping the code as simple as possible? Can you make a PR so we can see the diff?

EDIT, nevermind, I updated it to JS with no build. That one wasn't so bad! Better to not have separate files for the classes in that case.

Here's one class:

/**
 * @abstract
 * @template {object} T
 */
export class Foo {
  foo = "456"

  /**
   * @abstract
   * @returns {T}
   */
  method() {
    throw "subclass must implement"
  }

  doFoo() { this.foo; this.method() }
}

@abstract doesn't work yet (feature request). Here's the other class:

import { Bar } from "./Bar.js"
import { Foo } from "./Foo.js"

const FooBar = /** @type {typeof Foo<Bar>} */ (Foo)

export class Test extends FooBar {
  /** @override */
  method() {
    return new Bar()
  }
}

const b = new Test().doFoo()
b.logBar()

with terser comments, it could be:

export /*: abstract */ class Foo/*:<T extends object>*/ {
  foo = "456"

  /*: abstract */method() { //: T
    throw "subclass must implement"
  }

  doFoo() { this.foo; this.method() }
}
import { Bar } from "./Bar.js"
import { Foo } from "./Foo.js"

export class Test extends Foo/*:<Bar>*/ {
  /* :override */ method() {
    return new Bar()
  }
}

const b = new Test().doFoo()
b.logBar()

That still feels a little awkward and noisy to me. Here's an alternative that I like more:

//: abstract
export class Foo { //:<T extends object>
  foo = "456"

  //: abstract
  method() {} //: T

  doFoo() { this.foo; this.method() }
}
import { Bar } from "./Bar.js"
import { Foo } from "./Foo.js"

export class Test extends Foo { //:<Bar>
  //: override
  method() {
    return new Bar()
  }
}

const b = new Test().doFoo()
b.logBar()

Ideas are varied at the moment, but I think that last one is cleanest so far.

There would need to be rules that associate the comments to the parts they annotate. For example, the //: T annotates method's return value because that's the last thing on the same line that it can (say, we're not annotating the curly braces, but the function)

SIde by side:

/**
 * @abstract
 * @template {object} T
 */
export class Foo {
  foo = "456"

  /**
   * @abstract
   * @returns {T}
   */
  method() {
    throw "subclass must implement"
  }

  doFoo() { this.foo; this.method() }
}
//: abstract
export class Foo { //:<T extends object>
  foo = "456"

  //: abstract
  method() {} //: T

  doFoo() { this.foo; this.method() }
}

@theScottyJam
Copy link

I ended up spending some time working on this problem, and was able to put together a fork of TypeScript that allows TypeScript syntax to be inside of comments - it took more work than I thought it would :p. Anyways, it's up one NPM and GitHub.

As for putting JSDocs inside of a "TS Comment" (What I'm calling these /*:: ... */ /*: ... */ things), I ended up solving that issue by just having you close both the JSDoc comment and TS comment with the same closing comment token, then right afterwards you would re-open the TS comment, like this:

/*::
interface User {
  readonly username: string
  readonly birthday: Date
  /** @deprecated *//*::
  readonly age: number
}
*/

It's a little funky, but it works good enough for now.

@jakebailey
Copy link
Member

Given this is a pure fork and seems to have the same repo structure, is this change PR-able?

@theScottyJam
Copy link

Maybe not this fork specifically - it has things like changes to the README in order to give the npm package an updated README.

But I can make another fork that strips that kind of stuff out, then put together a PR.

@theScottyJam
Copy link

Also, the current fork contains a couple of extra command line options that let's you transform your project from TypeScripr to JavaScript with type comments and vice versa (using the includes and excludes lists from tsconfig.json to know what to convert) - I assume I'd need to strip that out as well as that wasn't part of the original feature request.

@piranna
Copy link

piranna commented May 7, 2024

@theScottyJam, have you added a flag to enforce using inline comments instead of regular Typescript syntax? Or that would be better implemented as a linter rule?

And since there's a flag to convert Javascript to Typescript and viceversa, does it add types to current Javascript projects?

@somebody1234
Copy link

@piranna if you're using inline comments you're probably writing JS anyway. so it's enforced in the sense that any TS syntax is invalid syntax

@theScottyJam
Copy link

@piranna For that first half of your question, yes, what @somebody1234 said - you'd typically be using JS files anyways, so it's already enforced in that sense (though the comment syntax should work in TS files too - there's not much use for them except, say, doing a slow manual transition or something - I haven't really tested that sort of thing much - yet).

And since there's a flag to convert Javascript to Typescript and vice-versa, does it add types to current Javascript projects?

All the converters do is add or remove comment delimiters (the /*:, /*::, or */ things) to existing code. They're not any smarter than that. They won't try and infer types or anything. So there's no converter from "JavaScript without types" to "JavaScript with types".

@piranna
Copy link

piranna commented May 7, 2024

@piranna if you're using inline comments you're probably writing JS anyway. so it's enforced in the sense that any TS syntax is invalid syntax

Yes, my intention is to write Javascript, not Typescript. My question was about writting Typescript, but with inline comments. But if all the Typescript syntax (not only types) can be put inside inline comments, then effectively you are writting Javascript with comments :-)

All the converters do is add or remove comment delimiters (the /*:, /*::, or */ things) to existing code. They're not any smarter than that. They won't try and infer types or anything. So there's no converter from "JavaScript without types" to "JavaScript with types".

So, if I have actual Javascript code, it will not add types comments, is that? And if I want them, I need to convert the Javascript code to Typescript, and later back to Javascript with types, is that correct? Do you know any tool that can fill all the Typescript types in their place?

@piranna
Copy link

piranna commented May 7, 2024

Do you know any tool that can fill all the Typescript types in their place?

Ok, I've found https://github.com/airbnb/ts-migrate and https://github.com/JoshuaKGoldberg/TypeStat, latest one is in active development and looks promising :-)

@trusktr
Copy link
Contributor

trusktr commented May 11, 2024

@theScottyJam since you are working on this, and considering that the new syntax is not JSDoc-compatible, any chance you'd please consider adding support for the more concise ideas I mentioned in these comments 🙏?

For example this one is a lot cleaner than the /**/ alternative would be:

//: abstract
export class Foo { //:<T extends object>
  foo = "456"

  //: abstract
  method() {} //: T

  doFoo() { this.foo; this.method() }
}

The /**/ format could be supported too, f.e. the following two would be the same, depending on formatting and style choice:

export class Foo { //:<T extends object>
export class Foo /*:<T extends object>*/ {

@theScottyJam
Copy link

One of the things I dislike about JSDocs-for-types is that it's a fairly different syntax from normal TypeScript, which makes it more difficult for an existing TypeScript user to pick up and start reading or writing it.

What it looks like you're suggesting, is to allow us to move the TypeScript syntax to different places in order to make it easier to put that syntax in a line comment instead of a block comment, such as moving the return type after the functions opening brace ({), or moving modifiers to their own line. This, to me, feels like we'd be falling into the same problem where there will be a lot of little rules around this TS-in-comments feature.

It can also cause clashes if we're not careful - in your above examples, you show that generic parameters can be put after the opening bracket, but you also show that return types can occupy the same location, so what if you want to do both?

// How would you do the following TypeScript code using TS-in-comments?
function fn<T extends object>(x: T): T { ... }

// Maybe like this? The two things are separated by a space?
function fn(x /*: T */) { //:<T extends object> T
// Or like this?
function fn(x /*: T */) { //:<T extends object> //: T
// Maybe this combination isn't allowed and you're forced to do something like this instead?
// picking one or the other to go after the "{" but not both?
function fn/*::<T extends object>*/(x /*: T */) { //: T
// Maybe generic functions always have to be written with their type in the correct spot,
// and the shorthand for moving generics after the "{" can only be used with classes?

We could come up with rules to define this behavior - but the fact that there isn't any clear-cut way to handle these scenarios worry me, and makes me less inclined to want to go that route. Plus, I don't really find the /*:: ... */ to be all that bad to use.


(Some vocabulary for this next part - The special comments where arbitrary syntax can go inside that's being proposed in this thread - I'm calling those "TS comments")

That being said - there is one shorthand syntax that I would really love to have (which I've seen others mention in different places) - and that's the ability to put the types of a function onto the preceding line, something kind-of like this:

//: (string, { x: number, y: number }) => string
function doThatThing(name, { x, y }) {
  ...
}

I hesitate to actually add anything like that for a few reasons:

  • A feature like this would be nice to have in normal ts files as well, so maybe it would be better to add this as new TypeScript syntax, as a separate proposal. Once it's added as native TypeScript syntax, we'd automatically be able to use the same feature with TS comments.
  • Or, we could add this feature as a TS-comment-only type of thing, and we don't provide native TypeScript syntax for it. This might make sense, since a syntax short-hand like this is a stronger need for JavaScript files than TypeScript files. Plus, a TypeScript user can always use TS comments in a TypeScript file to use this syntax if they wanted to do so.
  • Would adding support for this kind of syntax make the whole feature less likely to go in?
  • How much more complicated would it be to add new syntax? I've never tried it, so I don't know. I feel like I'd be able to update the parser just fine, but I worry about how difficult it would be to support all of the special editor tooling the TypeScript repo comes with (such as renaming variables and what-not) - it might just work, but maybe not, dunno. (This same concern applies with trying to move TypeScript syntax around to different places like you were proposing).

@trusktr
Copy link
Contributor

trusktr commented May 19, 2024

Indeed, we'd need to settle on some rules. I believe that the /*: */ format would be a fallback for scenarios when the code format does not allow for end-of-line comments. We'd also have end-of-line comments that can contain all parts of a type definition (f.e. for a function). Here are some examples for thought:

// We can use /*: */ comments:

function fn /*: <T extends object> */ (x /*: T */) /*: T */ { /*...*/ }

// Here's the same thing but with the function split onto separate lines:

function fn /*: <T extends object> */ (
  x /*: T */
) /*: T */ {
  // ...
}

// Here's the same thing as the previous, but with added documentation:

function fn /*: <T extends object> - description of T */ (
  x /*: T - the description for `x` (**note**, this is markdown) */
) /*: T - the return value description */ {
  // ...
}

// A rule for the end-of-line comments could be that they apply to the last
// annotatable position prior to them on that line, so the following would be
// equivalent to the previous sample:

function fn ( //: <T extends object> - description of T
  x //: T - the description for `x` (**note**, this is markdown)
) { //: T - the return value description
  // ...
}

// A mix can be used as desired. Here only the return annotation is an
// end-of-line comment:

function fn /*: <T extends object> - description of T */ (x /*: T - the description for `x` */) { //: T - the return value description
  // ...
}

// Now, how about for describing a function not inline? Maybe end-of-line
// comments could be used, and because there is nothing in front of them on the
// same line, they'd apply to the next item below. Note how in the next example
// I left the comment for <T> in the same spot, so it applies to the generic T
// parameter.

//: - This is a description for the function (it is *markdown*)
//: x: T - This comment has a name (between the ::) so it will apply to the function's `x` parameter
//: Perhaps this line is automatically a continuation of the description of `x` similar to JSDoc comments.
//: return: T - The description of the return value.
function fn ( //: <T extends object> - description of T
  x
) {
  // ...
}

// If we were to hoist the T parameter comment, it would fit into the overall
// comment for the function:

//: - This is a description for the function (it is *markdown*)
//: <T extends object>: - description of T (T is defined between the ::, but maybe that's not needed if the <> symbols are enough)
//: x: T - This comment has a name (between the ::) so it will apply to the function's `x` parameter
//: Perhaps this line is automatically a continuation of the description of `x` similar to JSDoc comments.
//: return: T - The description of the return value.
function fn (x) {
  // ...
}

// Maybe the "return" word is not required, so two colons would associate with
// the return:

//: - This is a description for the function (it is *markdown*)
//: <T extends object>: - description of T (T is defined between the ::, but maybe that's not needed if the <> symbols are enough)
//: x: T - This comment has a name (between the ::) so it will apply to the function's `x` parameter
//: Perhaps this line is automatically a continuation of the description of `x` similar to JSDoc comments.
//: : T - The description of the return value (no "return" comment name here).
function fn (x) {
  // ...
}

// Taking advantage of the rule that an end-of-line comment applies to the
// previous annotatble location, it could be convenient in various places:

const arr = [1,2,3] //: as const - this description is ignored, but could be useful
const foo = {
  // ...
} //: satifies Foo

// In this one we have both:

const bar /*: HTMLDivElement */ = document.querySelector('.foo') //: as HTMLDivElement

// These have the same result:

let a /*: number | null */ = 123
let a //: number | null
  = 123

// These all have the same result (using `a` from before)

let b /*: number */ = a /*:! - force a not null (this description is ignored) */ 
let b /*: number */ = a /*:!*/  // (no description)
let b /*: number */ = a /*!*/  // Maybe /*!*/ is a unique shorthand
let b //: number
  = a //:! - with a description (ignored)
let b //: number
  = a //:!
let b = //: number - this still annotates b (perhaps less desirable formatting)
  a //:!

// Here are some examples of end-of-line comments annotating the next thing:

//: number - some description
let b = a /*:! - force a not null (this description is ignored) */ 
//: number
let b = a /*!*/
//: SomeType - this is a special object
let o = {} //: as SomeType

//: abstract
class Base {
  //: number | null
  n = 123

  //: (a: number, b: string) => boolean - Do something.
  doSomething(a, b) {
  }

  //: - Do something else.
  //: a: number - some number 
  //: b: string - some string 
  //: :boolean - returns a boolean indicating something
  doSomethingElse(a, b) {
  }
}

// Finally here is a terser option without descriptions for the function
// definition. This would be similar to JSDoc using @type instead all the individual
// parts (@param, @return, etc):

//: <T>(x: T) => T - This is a description for the function (it is *markdown*)
function fn (x) {
  // ...
}

I'm thinking about this from the perspective of a user: what would make it as concise as possible? On the other hand, I'm not sure if this makes it more complicated to implement, but as a user I would enjoy having terser comments.

Here's a hand-crafted basic example of what syntax highlight could look like:

Screenshot 2024-05-18 181318

@trusktr
Copy link
Contributor

trusktr commented May 19, 2024

One more with types inline instead of in header comments:

//: - This is a description for the function (it is *markdown*)
//: T: - description of T (T is defined between the ::, but maybe that's not needed if the <> symbols are enough)
//: x: - This comment has a name (between the ::) so it will apply to the function's `x` parameter
//: : - The description of the return value
function fn /*:<T extends Object>*/ (x /*: T*/ ) /*: T*/ {
  // ...
}

// Spaces are optional:

//:- This is a description for the function (it is *markdown*)
//:T:- description of T (T is defined between the ::, but maybe that's not needed if the <> symbols are enough)
//:x:- This comment has a name (between the ::) so it will apply to the function's `x` parameter
//::- The description of the return value
function fn /*:<T extends Object>*/ (x /*:T*/ ) /*:T*/ {
  // ...
}

Type definitions:

// Maybe the rule is, if this is on a line on its own (not annotating anything) then its just type space for TS type syntax:
/*:
export interface Foo { ... }
export type Other = Foo | string
*/

//: <T extends Foo>(x: T) => T
function fn (x) {
  // This one is scoped inside the function (can't use `export`)
  /*: interface Bar { ... } */

  // ...
}

TSDoc broke compatibility with original JSDoc, I mean they are basically incompatible in various ways (namely TS type syntax is not part of JSDoc). On that note, it might make just as much sense to make a new (terser) syntax and people can make new tooling for it (just like they had to for TSDoc because JSDoc tooling didn't work).

Any other ideas/thoughts?

@piranna
Copy link

piranna commented May 19, 2024

Definitely i like It.

@texastoland
Copy link
Contributor

texastoland commented May 20, 2024

My opinions to help reign in scope a little bit:

I ended up spending some time working on this problem, and was able to put together a fork of TypeScript that allows TypeScript syntax to be inside of comments

@theScottyJam I applaud your initiative 🎆

One of the things I dislike about JSDocs-for-types is that it's a fairly different syntax from normal TypeScript, which makes it more difficult for an existing TypeScript user to pick up and start reading or writing it.

JSDoc has also been a maintenance burden creating distinct syntax for every addition to .ts syntax.

I hesitate to actually add anything like that for a few reasons:

I agree the more minimal the better for MVP 💯 Important to get this accepted in principle before bike shedding design and implementation of more convenient shorthand.

function fn /*: <T extends object> */ (

/*: ... */ is shorthand for /*:: : ... */ so this line should be /*:: <T ...> */.

function fn /*:: <T extends object> - description of T */ (

The XY problem this solves is to use types-in-comments alone instead of in conjunction with JSDoc. That doesn't seem achievable short-term. Also consider the example above would require case analysis of the construct being parsed to associate the docstring with the correct identifier. That's similar to the maintenance cost already inherent in JSDoc.

x //: T

This I conceptually love though!

A rule for the end-of-line comments could be that they apply to the last annotatable position prior to them on that line,

The XY problem here is to reduce verbosity without breaking Prettier's default formatting. Last annotatable condition would be ambiguous though consider:

function fn ( //:: <T extends object>
  x //: T

But it would be valid to have an access modifier like private in that position. Alternatively the type content could be specified to immediately precede any terminating punctuation like braces, ,, ;, =, etc.

  /** @deprecated *//*::

This syntax is unfortunate too and should probably use a different character sequence for nested comments. <!--: is the only 1 I could think of today.

All the other ideas feel out of scope for an initial version.

@theScottyJam
Copy link

theScottyJam commented May 21, 2024

I was initially worried about the <!--: suggestion, because, in the browser, in non-es-modules, <!-- is technically a valid way to create a comment, but I toyed around with it a bit, and it seems that <!-- doesn't do anything when its nested inside of a JS comment, so no conflict there.

Another option would be to allow jsdocs to be written after triple slashes (both inside and outside of the TS comments).

/*::
interface User {
  /// @deprecated
  readonly age: number
}
*/

It would make for a fairly nice solution, but it could also cause conflicts with TypeScript's existing triple-slash directives.

I'll spitball a few more options.

Another single-line option that tries to look similar to /**? (Not a huge fan of this one after seeing it)

/*::
interface User {
  //** @deprecated
  readonly age: number
}
*/

Mangle the closing token somehow?

/*::
interface User {
  /** @deprecated * /
  readonly age: number
}
*/

Swap the star character?

/*::
interface User {
  /:: @deprecated :/
  readonly age: number
}
*/

Swap the slash character? (I kind of like this one)

/*::
interface User {
  \** @deprecated *\
  readonly age: number
}
*/

@theScottyJam
Copy link

theScottyJam commented May 21, 2024

Here's a draft PR: #58601

It still has some broken tests that I'm trying to sort through, and I haven't added test coverage for the new stuff yet, and there's a few other changes I know will need to be made.

If possible, I would like some guidance on a couple of issues. Perhaps, first, let me give some background on how I implemented this PR.

How I implemented the PR

Whenever the scanner runs into a /*:, /*::, or */ sequence, it'll just ignore it and move on.

In src/compiler/program.ts there's existing logic that'll report an error if it runs across TypeScript syntax inside of a JavaScript file. I hijacked this section of the code to add the error-reporting that this PR needs - as it walks across the tree, looking for TypeScript syntax, I made it so it'll additionally scan the regions between tokens looking for TS comment delimiters. It records the comment delimiters it finds so it can know if a particular node is in a TS comment when it shouldn't be or vice-versa, and then it'll report the appropriate errors.

Problem 1

Consider the following expression:

a */* XXX */ b

Without the block comment in the middle, this simply evaluates to a * b. Since I updated the scanner to ignore closing block comment sequences (*/), it'll cause the above code to appear like a * XXX b to the parser, which is a problem.

Ideally, I'd only ignore the */ sequence if I know that we're currently inside of a TS block comment, but that would require the scanner to be aware of information about its surroundings - something which it currently does not do. I could update the scanner so it keeps track of every comment delimiter it comes across, thus letting it know if it's currently inside or outside a TS comment at any point in time. This would require the scanner to follow two rules - 1. It never skips over regions unless it has previously scanned those regions. (seems plausible) 2. It'll never scan the inside of one token, find and record a comment delimiter, then backtrack, realizing that that was the wrong way to scan it - e.g. if I have the string "a*/b", and the scanner starts scanning at the *, it'll incorrectly find a closing comment delimiter and permanently record it, even if, later, the parser backtracks and makes the scanner start from the opening quote instead. (It's hard for me to tell if this guarantee can be made or not, which is why I'm asking).

Even if this can be done, I'm not sure if it's the right way to go - I assume the scanner was intentionally designed to not carry around state like this, and perhaps that's a design decision that we don't want to change. I just don't know yet how else to implement the feature.

Problem 2

At the moment, it'll always try and parse the contents of a /*: ... */ and /*:: ... */ section, even if you don't have JavaScript type-checking enabled. No type errors will be reported, but syntax errors still can, i.e. the perfectly valid JavaScript program console.log(2 /*: huh */); will cause your editor to report odd errors. I know this will be a problem, because Flow also uses the /*: ... */ syntax, and vs-code uses TypeScript for JavaScript language support, which means Flow users using these comments with vs-code will not have a lot of fun.

The issue could be partially fixed by having configuration from your tsconfig.json get passed into the scanner, then the scanner can change behavior depending on if you have the allowJS/checkJS flags set or not. TypeScript also adds support for the // @ts-check directive, which seems to enables typechecking from that point and on in a file - to support that, I suppose I could have the parser tell the scanner whenever the parser sees one of these (or when the parser sees a // @ts-nocheck directive), thus telling the scanner to switch behavior at those points. Here we'll run into a similar issue as we saw in problem 1, where this solution can only work if the scanner doesn't jump around in wild and unpredictable manners.

An alternative solution would be to invent a new tsconfig option for this feature, and to not provide a JavaScript directive to go with it - you either globally enable it, or you don't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.