tc39 / proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
https://tc39.es/proposal-type-annotations/
4.27k stars 47 forks source link

pros/cons of type comments *inside* regular comments? #192

Open trusktr opened 1 year ago

trusktr commented 1 year ago

For example, over in https://github.com/microsoft/TypeScript/issues/48650#issuecomment-1721578187, I've described types-in-comments ideas that both:

  1. are more concise than JSDoc types
  2. include space for documentation information

For convenience, I've pasted that comment below (and it is actually very TS-specific now thinking about it), but also see that issue for some more thoughts.

My question is, how viable is this approach? Can a standard syntax for use inside-of-existing comments be a thing? Why or why not?

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

From an end user perspective, @dfabulich's second example is simpler, cleaner, less noisy. I acknowledge it would be more work than essentially only ignoring comment markers, but the end user experience could be improved.

What about documentation?

It feels like we should also consider alternative documentation comment syntax to go along with the new type comment syntax, otherwise mixing the two doesn't seem to accomplish much. Here's an example with JSDoc comments, but new type comment syntax:

/**
 * @param a - description for a
 * @param b - description for b
 * @param c - description for c
 */
function method(a, b, c) {
  //: number, number, number => number
  return a + b + c;
}

Here's an with both new doc and type comment syntax:

function method(a, b, c) {
  //: number, number, number => number
  //: a - description for a
  //: b - description for b
  //: c - description for c

  let foo = 123 // is this affected by the previous comment or not? 123 not assignable to function?

  return a + b + c;
}

Alternative:

//: a:number - description for a
//: b:number - description for b
//: c:number - description for c
function method(a, b, c) {
  let foo = 123
  return a + b + c;
}

Here are more examples of potential doc+type comments:

/** :string|boolean - the description */
let foo = "foo"
foo = false

or

// :string|boolean - the description
let foo = "foo"
foo = false

Suppose the strip-markers stuff still all works,

function foo(a /*:number*/, b /*:string*/) /*:boolean*/ {...}

but with alternatives like above and

function foo(
  a, //:number
  b, //:string
) { //:boolean
  ...
}

or a combo, depending on taste:

function foo(
  a, //:number
  b, //:string
) /* :boolean */ {
  ...
}

Or

function foo(
  a, //:number - with description 
  b, //:string - with description
) { //:boolean - with description
  ...
}

Similar to one of my above examples, but with an explicit return type+doc:

//: a:number - description for a
//: b:number - description for b
//: c:number - description for c
//: :string - description of return value
function method(a, b, c) {
  return "Val:"+a + b + c;
}

Maybe we don't need some of the colons:

// a: number - description for a
// continued on multiple lines with a code sample:
// ```js
// method(a,b,c)
// ```
// b: number - description for b
// c: number - description for c
// :string - description of return value
function method(a, b, c) {
  return "Val:"+a + b + c;
}

Depending on the code formatting (new lines or not, etc), and on whether or not documentation is needed, the desirable format to use can vary, so a few different ways of commenting could be beneficial.

Having a more concise type comment syntax, but having to fall back to JSDoc for documentation, leaves some sort of awkward feeling.

The more concise this can be, the better (for us end users).

egasimus commented 1 year ago

Thanks for reposting this here.

I'm in total support of this proposal, rather than breaking changes to the language syntax. It's backwards-compatible, familiar, and featureful - by building atop existing work rather than contributing to a proliferation of standards.

theScottyJam commented 1 year ago

I don't feel like you have to overly worry about trying to support both comments and types into this same syntax.

I use both TypeScript and JSDocs at the same time - TypeScript for my types, and JSDocs for my documentation. I see no reason why the same can't be done here - type comments for types and JSDocs for documentation.

If you're providing per-parameter documentation for a particular function, then JSDocs isn't all that heavy - the "text-to-overhead" ratio would be relatively low. Things only become an issue if you don't need to provide per-parameter documentation (maybe it's a private internal function, where you really don't need to provide a ton of documentation for it, or maybe a simple overall description works better than describing each and every parameter).

theScottyJam commented 1 year ago

Also, I really wished what you are proposing was being considered more seriously.

The comment syntax you're proposing seems very nice to use. I would prefer using that over TypeScript with a compile step any day.

msadeqhe commented 1 year ago

I like your suggestion, because:

But it's verbose to write.

msadeqhe commented 1 year ago

Suppose the strip-markers stuff still all works,

function foo(a /*:number*/, b /*:string*/) /*:boolean*/ {...}

but with alternatives like above and

function foo(
  a, //:number
  b, //:string
) { //:boolean
  ...
}

or a combo, depending on taste:

function foo(
  a, //:number
  b, //:string
) /* :boolean */ {
  ...
}

Or

function foo(
  a, //:number - with description 
  b, //:string - with description
) { //:boolean - with description
  ...
}

Similar to one of my above examples, but with an explicit return type+doc:

//: a:number - description for a
//: b:number - description for b
//: c:number - description for c
//: :string - description of return value
function method(a, b, c) {
  return "Val:"+a + b + c;
}

Maybe we don't need some of the colons:

// a: number - description for a
// continued on multiple lines with a code sample:
// ```js
// method(a,b,c)
// ```
// b: number - description for b
// c: number - description for c
// :string - description of return value
function method(a, b, c) {
  return "Val:"+a + b + c;
}

Now, I'm trying to translate that example from regular comments to :-style comments:

function foo(a: number, b: string): boolean { /* ... */ }

function foo(
    a: number,
    b: string
): boolean {
    //...
}

function foo(
    a: number "with description",
    b: string "with description"
): boolean "with description" {
    //...
}

function method(
    a: number "description for a" detail(
        continued on multiple lines with a code sample:
        ```js
        method(a,b,c)
        ```),
    b: number "description for b",
    c: number "description for c"
): string "description of return value" {
    return "Val: " + a + b + c;
}

Only the syntax is shorter, but we are free to mix it with regular comments:

function method(
    a: number "description for a",
    // continued on multiple lines with a code sample:
    // ```js
    // method(a,b,c)
    // ```
    b: number "description for b",
    c: number "description for c"
): string "description of return value" {
    return "Val: " + a + b + c;
}

I mean, with the right rules for :-style comments, we would have improved JS comments.

trusktr commented 1 year ago

It seems that way that the descriptions make the code harder to read, more noisy. Also, descriptions can be multi-line, and contain markdown, etc. (VS Code formats multi-line arkdown when displayed in tooltips, etc).

Let's see: taking this example:

function foo(
  a, //:number - with description 
) { //:boolean - with description
  ...
}

it might be like this with long description (the start position of the //:boolean comment applies to the last position that can be typed, which happens to be the return value),

function foo(
  a, //:number - with a long description that
     // spans multiple lines
) { //:boolean - with a long description that
    // spans multiple lines and even has a markdown example:
    // ```js
    // if (foo(123)) do()
    // ```

  ...
}

or perhaps like this (note that in the comment that is above the function, the lack of a named item implies that it documents the return value):

//:boolean - with a long description that
// spans multiple lines and even has a markdown example:
// ```js
// if (foo(123)) do()
// ```
function foo(
  //:number - with a long description that
  // spans multiple lines
  a, 
) {
  ...
}

How would multi-line descriptions work with : type comments?

azder commented 1 year ago

Whichever syntax you pick, if you inline them, you're just propagating the same... how should I call it... non-separation of concerns?

I mean this:

    const foo = ( a: number ): number => a;

vs

    //: foo = number => number;

    const foo = a => a;

Why complicate the code for eyes scan? Does that improve the compiler scanning in some way? Does it have any drawback aside from "it feels weird because I'm trained to look at it the other way"?

I ask this mostly because since I've learnt a bit of Haskell, I've been using TS types like this:

    type Foo = (a: number) => number;

    const foo: Foo = a => a;

and it has been quite simpler to not bother with keywords like interface meant to look familiar to people coming in from Java/C# and similar places. I think there might be a benefit to this kind of simplicity and separation of the type info from the implementation, as a sort of "header file"... though, that sounds odd

theScottyJam commented 1 year ago

I generally agree (and really wished Typescript let you put a function's type in the preceding line). But there are cases where inline types can be nice, such as with callbacks.

azder commented 1 year ago

@theScottyJam How would you declare a callback's type? Wouldn't it be part of the receiving function's declaration? How often do you need to specify a particular lambda's type vs the compiler inferring it?

In TS that would be something like

    type Receiver = ( callback: ( a : number ) => number ) => (( a : number) => number);

    const receiver: Receiver => b => b;
    const result = receiver( c => c );

or

    type Callback =  ( a : number ) => number;
    type Receiver = ( callback: Callback ) => Callback;

    const receiver: Receiver => b => b;
    const result = receiver( c => c );

so in the simplified haskelian comment/type declaration, you'd have:

    //: callback = number => number;
    //: receiver = callback => callback;

    const receiver = $ => $;
    const result1 = receiver( $ => $+$ );
    const result2 = receiver( receiver );

So, how often would you need to force a type there vs inferrence? Something like

    //: receiver = <T>( T => T );

    //: callback = string => string;
    //: identity = receiver< callback >;
    const identity = $ => $;

    // the type of String wold be any=>string, so to narrow it one might do
    // a
    const r1 = identity( String /*: string=>string */);

    // or b, just let it be inferred as the type of callback is part of the identity declaration
    const r2 = identity( String );

    // or c, have some unambiguous syntax that does it before the invocation
    // imagine the colon squiggly meaning it's temporary, only for the following line

    //:~ String = callback
    const r3 = identity( String );

Of course, I'm always in favor of separating type info from implementation detail as I think that makes for nicer code to parse both visually and probably simpler for compilers to not have too complicated parser if it's meant to only accept for an example line comments that start with only //: and not inline one with /*: / as well.

How would /*//: play out in that case, I wonder...

BTW, @theScottyJam could you elaborate on what's that TS doesn't allow that you wish it did? Have an example?

theScottyJam commented 1 year ago

So, how often would you need to force a type there vs inferrence?

Good point. I just looked through one of my repos, and found the answer to be "very little". The only scenarios I could find in this particular repo were places where my callback was being provided with a value of the unknown type, and I said in the callback parameters to treat it like the any type instead. I could also imagine scenarios (like you mentioned) where my callback is being given a value of type any and I want to change that into something more specific in the parameter type.

But that's about it - it seems I really only set the type of a callback's parameter when I'm going to or from the any type, which isn't a very common thing for me to do. I'm struggling to think of any other kind of scenario where one might want to specify the parameter types on a callback.

BTW, @theScottyJam could you elaborate on what's that TS doesn't allow that you wish it did? Have an example?

Just the ability to put a function's type on the line before, instead of mixing it into the same line.

// TypeScript today
function fetchUser(db: Database, id: string, { skipCache = false }: { skipCache: boolean }): User {
  ...
}

// A more ideal syntax
: (Database, string, { skipCache: boolean }) => User
function fetchUser(db, id, { skipCache = false }) {
  ...
}

// Though, honestly, having it in a comment like you're proposing is just as good for me.
//: (Database, string, { skipCache: boolean }) => User
function fetchUser(db, id, { skipCache = false }) {
  ...
}
msadeqhe commented 1 year ago

How would multi-line descriptions work with : type comments?

We may use Template Literals and enclose them with backtick `:

function foo(
  a: number
   : `with a long description that
      spans multiple lines`
): boolean
 : `with a long description that
    spans multiple lines and even has a markdown example:
    \`\`\`js
    if (foo(123)) do()
    \`\`\` ` {

  // ...
}

Or we may mix them with regular comments:

function foo(
    a: number
    /* with a long description that
       spans multiple lines */
): boolean
 /* with a long description that
    spans multiple lines and even has a markdown example:
    ```js
    if (foo(123)) do()
    ``` */ {

    // statements...
}

Although that's possible, but I'm agree that :-style comments would lost their advantage when writing documentation descriptions.