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

Can we have a simple syntax to rule them all? `:comment`. #188

Open msadeqhe opened 1 year ago

msadeqhe commented 1 year ago

What do you think about this syntax? Let's find a rule for it:

(code:comment,code:comment,...,code:comment)

These are the rules:

In this way, we can write multiple inner comments within a block between ().

Similarly, we can have the following syntax for {} and [] blocks:

{code:comment,code:comment,...,code:comment}
[code:comment,code:comment,...,code:comment]

In the first syntax, comment will end with ,, ; or } except if they are inside another nested block like (), {}, [], <>, "" or ''.

In the second syntax, comment will end with ,, ; or ] except if they are inside another nested block like (), {}, [], <>, "" or ''.

Until now, it was really the syntax of TypeScript, Flow, etc.

EDIT: Inner comments between (), {} and [] are only allowed for declarations. That's the syntax of type checkers. Javascript engines easily can distinguish between declarations and expressions such as object creations, function calls and etc.

So, what if comment isn't within a block between (), {} or []?

code:comment code

This needs to be resolved. These comments can be written either within declarations or expressions and statements. We can resolve it in one of the following ways:

Considering the above rules, let's write some JavaScript code with type information:

function name(a: string, b: string): "string" { ... }

:{interface someone {
    name: string;
}}

class something:<T, U> {
    a: T;
    b: U;
}

let x = something:<number, number>();

In this way, the type as comment would have a general syntax for type checkers.

EDIT: The final grammar is ...

In a nutshell the rules are:

  • Statement Comment (:: comment):
    • :: must be stand alone, so only ; or white-space can be before ::.
    • The comment will end before ; or newline.
  • Expression Comment (code :comment separator code):
    • The comment may not start with an opening bracket (quote) (either (, [, {, <, " or ').
    • The comment will end before ,, ;, = or { (as separator).
    • The comment will end before ) (as separator) if it's enclosed within ().
    • The comment will end before ] (as separator) if it's enclosed within [].
    • The comment will end before } (as separator) if it's enclosed within {}.
  • Enclosed Expression Comment (code :comment code):
    • The comment may start with an opening bracket (quote) (either (, [, {, <, " or ').
    • The comment will end with the matching closing bracket (quote) (correspondingly either ), ], }, >, " or ').
    • The end of nested expression comment may be extended with a valid JS operator until it ends with the rules of Expression Comment.
  • Declaration Comment (code identifier <comment> code):
    • They are only valid within class, function and variable declarations.
    • The comment must be enclosed within <>, and it must be directly after the identifier.

In all cases:

  • : doesn't start a comment within Object Literals, Ternary Operators and Label Statements.
  • If the comment contains a nested bracket (quote) pair, it won't end in the middle of the nested bracket (quote) pair (e.g. (), [], {}, <>, "" or '').
  • The behavior of quote pairs (e.g. "" and '') are like string literals. Their content cannot be multiple lines. Also escape sequences are meaningful in their content.

For example:

  • Statement Comment:
    • :: comment is equal to nothing.
  • Expression Comment:
    • (code:comment) is equal to (code).
    • code:comment, code is equal to code, code.
  • Enclosed Expression Comment:
    • code:(comment) code is equal to code code.
    • :(comment) & comment is equal to nothing.
  • Declaration Comment:
    • class name<comment> { ... } is equal to class name { ... }.
    • function name<comment>(...) { ... } is equal to function name(...) { ... }.
    • let name<comment> = ... is equal to let name = ....
    • Although we don't have this feature in TS, Flow, etc yet, but it's a possibility.

Additionally the following part of the proposal from README.md can also be included:

  • Optional arguments arg?:type
  • Non-nullable assertions obj!.member
  • this argument

In this way, most of the written code in current TypeScript, Flow, etc will be supported by JavaScript.


This is an example, what it would look like:

import: type (InterfaceX, TypeAlias) { ClassA, ClassB } from 'somewhere'

:: interface Abc<T> {
    name(): string;
    [i: T]: number;
}

function check<T>(a: { x: number , y: number } & { z: number }): classA<T> | undefined {
    // statements...
}

class Base<T> {
    constructor(width: T, height: T) {
        // statements...
    }

    // declarations...
}

class X<T> extends Base<T>: implements Abc<T> {
    @readonly count: number = 10;

    id(): string {
        return (count: as string);
    }

    // declarations...
}

// This part is not yet a feature of TS, Flow, etc.
let Pi<T> = (3.141592654: T);
let p = Pi:<number>;

let o = new Base:<number>(10, 2);
let u = (10: satisfies number);
spenserblack commented 1 year ago

Similarly, we can have the following syntax for {} and [] blocks:

{code:comment,code:comment,...,code:comment}

Maybe I'm misunderstanding your suggestion, but doesn't this conflict with objects?

{key:value}
msadeqhe commented 1 year ago

Similarly, we can have the following syntax for {} and [] blocks:

{code:comment,code:comment,...,code:comment}

Maybe I'm misunderstanding your suggestion, but doesn't this conflict with objects?

{key:value}

Sorry. I forgot to mention that inner comments within (), {} and [] are allowed only for declarations, because type as comments are useful for declarations. I'll update my suggestion to explain it. Thanks.

msadeqhe commented 1 year ago

In a nutshell, the syntax of the suggestion is like this:

With comment Without comment Which part was comment?
(code:comment) (code) :comment
{code:comment} {code} :comment
[code:comment] [code] :comment
code1:(comment) code2 code1 code2 :(comment)
code1:{comment} code2 code1 code2 :{comment}
code1:[comment] code2 code1 code2 :[comment]
code1:<comment> code2 code1 code2 :<comment>
code1:"comment" code2 code1 code2 :"comment"
code1:'comment' code2 code1 code2 :'comment'
code1:comment, code2 code1, code2 :comment
code1:comment; code2 code1; code2 :comment
code1:comment= code2 code1= code2 :comment

They are sorted by precedence, and they are comments only outside object construction (e.g. outside {var: value}).

Also both ,, ; and = will be treated as a part of comment if they are within nested (), {}, [], <>, "" or ''.

For example:

With comment Without comment Which part was comment?
(code1:(comment, comment), code2) (code1, code2) :(comment, comment)
(code1:comment (comment, comment), code2) (code1, code2) :comment (comment, comment)

EDIT: I fixed the syntax in the table.

msadeqhe commented 1 year ago

Why am I suggest this syntax? Because this syntax is close to TypeScript, Flow, ... with some changes to make it a general syntax for all possible types as comments. Additionally the rules are simple to follow.

Optionally JavaScript may define a standard format for the content of comment, so the way we specify the types as comments will be similar in all type checkers. Also type checkers can have their own additional type checking formats.

msadeqhe commented 1 year ago

In a nutshell, the syntax of the suggestion is like this:

With comment Without comment Which part was comment?
(code:comment) (code) :comment
{code:comment} {code} :comment
[code:comment] [code] :comment
code1:(comment) code2 code1 code2 :(comment)
code1:{comment} code2 code1 code2 :{comment}
code1:[comment] code2 code1 code2 :[comment]
code1:<comment> code2 code1 code2 :<comment>
code1:"comment" code2 code1 code2 :"comment"
code1:'comment' code2 code1 code2 :'comment'
code1:comment, code2 code1, code2 :comment
code1:comment; code2 code1; code2 :comment
code1:comment= code2 code1= code2 :comment

They are sorted by precedence, and they are comments only outside object construction (e.g. outside {var: value}).

Also both ,, ; and = will be treated as a part of comment if they are within nested (), {}, [], <>, "" or ''.

For example:

With comment Without comment Which part was comment?
(code1:(comment, comment), code2) (code1, code2) :(comment, comment)
(code1:comment (comment, comment), code2) (code1, code2) :comment (comment, comment)

EDIT: I fixed the syntax in the table.

The rules can be combined. For example, the following line:

code0 (code1: (comment, comment), code2: comment): "comment" code3

... is equal to this after ignoring comments:

code0 (code1, code2) code3

The above line can be a function declaration:

// function name(a: (comment, comment), b: comment): "comment" { return a + b; }
   function name(a                    , b         )            { return a + b; }

In which:

msadeqhe commented 1 year ago

I've changed the syntax of the example from README.md:

let x: string;

function equals(x: number, y: number): "boolean" {
    return x === y;
}

:{
    interface Person {
        name: string;
        age: number;
    }

    type CoolBool = boolean;
}

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    getGreeting(): "string" {
        return `Hello, my name is ${this.name}`;
    }
}

function all(a: Set<string>,
             b: { name: string, age: number },
             c: number[],
             d: (x: string): string,
             e: new (b: Bread): Duck,
             f: [number, number],
             g: string | number,
             h: Named & Dog,
             j: T[K]) {
    // ...
}

function split(str: string, separator?: string) {
    // ...
}

// TypeScript
let value = 1 :as number;

// Flow
let value = 1;
(value: number);

// assert that we have a valid 'HTMLElement', not 'HTMLElement | null'
document.getElementById("entry")!.innerText = "...";

:{
    type Foo<T> = T[];

    interface Bar<T> {
        x: T;
    }
}

function foo:<T>(x: T) {
    return x;
}

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

// Types Annotations - example syntax solution
add:<number>(4, 5);
new Point:<bigint>(4n, 5n);

function sum(this: SomeType, x: number, y: number) {
    // ...
}

So, in a nutshell, the difference between this suggestion and the proposal from README.md are:

  1. Return types should be inside "" (or any other bracket). (NOTE 1)
  2. Template arguments and parameters must be after : instead of ::. (NOTE 2)
  3. interfaces, types, ... should be inside :{/*here*/} or any other syntax. (NOTE 1)
  4. The syntax of type conversion must be changed from VAR as TYPE to either one of:
    • VAR :as TYPE, if it ends with , or ;.
    • VAR :(as TYPE)
    • (VAR: TYPE) (similar to Flow syntax)
    • or any other syntax. (NOTE 1)
  5. The syntax of function type must be changed from (ARGS) => TYPE to either one of:
    • (ARGS): TYPE
    • ((ARGS) => TYPE)
    • keyword (ARGS) => TYPE (keyword can be a meaningful name such as function).
    • or any other syntax (NOTE 1)

NOTE 1: The above syntax changes depend on the decision of type checkers to choose how to express the types as they want. For example, TypeScript may choose to wrap the return type inside "" but Flow may choose to wrap them inside '', or any other type checker may choose to wrap them inside () or ...

NOTE 2: I have to explain about case 2. When the template arguments are inside the comment, we may ommit :. For example:

// Multiple `:` won't end the comment in function argument list.
function all(a: Set:<string>) { /* ... */ }

:{
    type Foo:<T> = T[];

    interface Bar:<T> {
        x: T;
    }
}

We can omit : from them, because they are already a comment:

function all(a: Set<string>) { /* ... */ }

:{
    type Foo<T> = T[];

    interface Bar<T> {
        x: T;
    }
}

Additionally the following part of the proposal from README.md can also be included:

EDIT: The syntax of function types is changed.

msadeqhe commented 1 year ago

I should mention that with multiple : we can extend the comment. For example:

With comment Without comment Which part was comment?
let a: (x: string) => string; let a => string; (INVALID) : (x: string)

In above example, => string is not a part of comment, so it's invalid. To make it valid, we can extend the comment part with a sequence of comments:

With comment Without comment Which part was comment?
let a: (x: string) :=> string; let a; : (x: string) :=> string

If you feel (x: string) :=> string is ugly for function types, that's why we should change the syntax of function types to simply one of the following options, or any other syntax desired by type checker:

With comment Without comment Which part was comment?
let a: (x: string): string; let a; : (x: string): string
let a: ((x: string) => string); let a; : ((x: string) => string)
let a: function (x: string) => string; let a; : function (x: string) => string

BTW, I'm thinking about alternative ways such as allowing a special case for function types to be treated as comments.

msadeqhe commented 1 year ago

BTW, I'm thinking about alternative ways such as allowing a special case for function types to be treated as comments.

OK, I find a solution. The alternative way to solve this is if there is an operator after () in code1:(comment) code2, then the comment part will be extended until it reaches to one of ,, ; or =.

With comment Without comment Which part was comment?
code1:(comment) op comment, code2 code1, code2 :(comment) op comment
code1:(comment) op comment; code2 code1; code2 :(comment) op comment
code1:(comment) op comment= code2 code1= code2 :(comment) op comment

op can be any operator such as =>, |, &, ...

For example:

let name: (string) => string = /*...*/;

Its variable is name and its type is a function which gets a string an returns a string. The whole : (string) => string is the comment part, because operator => exists after (), the comment will ends with =.

The same set of rules can be applied to :{comment}, :[comment], :<comment>, :"comment" and :'comment'.

These rules also is useful to write types such as:

let x: (A & B) | C = /*...*/;

function caller(x: (A & B) | C,
                y: { name: string, age: number } & { id: number },
                z: "true" | "false") {
    // ...
}

So this restriction (change) is relaxed by the new rules:

  1. The syntax of function type must be changed from (ARGS) => TYPE to either one of:
    • (ARGS): TYPE
    • ((ARGS) => TYPE)
    • keyword (ARGS) => TYPE (keyword can be a meaningful name such as function).
    • or any other syntax (NOTE 1)

But these restrictions (changes) are still required:

So, in a nutshell, the difference between this suggestion and the proposal from README.md are:

  1. Return types should be inside "" (or any other bracket). (NOTE 1)
  2. Template arguments and parameters must be after : instead of ::. (NOTE 2)
  3. interfaces, types, ... should be inside :{/*here*/} or any other syntax. (NOTE 1)
  4. The syntax of type conversion must be changed from VAR as TYPE to either one of:
    • VAR :as TYPE, if it ends with , or ;.
    • VAR :(as TYPE)
    • (VAR: TYPE) (similar to Flow syntax)
    • or any other syntax. (NOTE 1)
msadeqhe commented 1 year ago

So, in a nutshell, the difference between this suggestion and the proposal from README.md are:

  1. Return types should be inside "" (or any other bracket). (NOTE 1)

The problem for this one is a little hard to resolve, it needs to be context aware, because unfortunately the return type of a function doesn't have a separator unlike arguments and parameters (which are separated by ,), member variables (which are separated by ;) and variable declarations (in which its value is after =).

trusktr commented 1 year ago

I love how much thought you put into this, and it is a very interesting idea.

msadeqhe commented 1 year ago

Thanks. I hope to make the rules as simple and general and compatible with TypeScript, Flow, etc as possible.

msadeqhe commented 1 year ago

So, in a nutshell, the difference between this suggestion and the proposal from README.md are:

  1. Return types should be inside "" (or any other bracket). (NOTE 1)

The problem for this one is a little hard to resolve, it needs to be context aware, because unfortunately the return type of a function doesn't have a separator unlike arguments and parameters (which are separated by ,), member variables (which are separated by ;) and variable declarations (in which its value is after =).

To solve this problem, we can exclude a special pattern from the :comment syntax. Because of the syntax of function declarations in JavaScript, we have to make a special rule for {} like this with a higher precedence than the last rule:

With comment Without comment Which part was comment?
code1:comment {code2} code1 {code2} :comment

Simply that means in a function declaration or where we have something like code1 {code2}, we can place a comment before {...}. So the table of rules for the :comment syntax will be like this.

With comment Without comment
openblock code:comment closeblock openblock code closeblock
code1:open comment close op comment sep code2 code1 sep code2
code1:open comment close code2 code1 code2
code1:comment {code2} code1 {code2}
code1:comment sep code2 code1 sep code2

Or in this format (bold and italic text is the comment):

openblock code:comment closeblock
code1:open comment close op comment sep code2
code1:open comment close code2
code1:comment {code2}
code1:comment sep code2

In which:

The table is sorted by precedence. The first row has the highest and the last row has the lowest precedence.

As an example for the above rules if we have parentheses () and , as the separator and => as the operator:

With comment Without comment Which part was comment?
(code:comment) (code) :comment
code1:(comment) => comment, code2 code1, code2 :(comment) => comment
code1:(comment) code2 code1 code2 :(comment)
code1:comment {code2} code1 {code2} :comment
code1:comment, code2 code1, code2 :comment

In the same way we can rewrite the rules for other brackets and separators and operators.

So within function declaration:

function something(): string { /* statements... */ }

: string is the comment and the comment ends with { /* statements... */ }. But in the following example:

function something(): T[K] { /* statements... */ }

: T[K] is the comment and the comment doesn't end with [K], because there isn't a rule for it.

Now consider this example:

function something(): { x: number } & { y: number } { /* statements... */ }

: { x: number } & { y: number } is the comment because:

  1. There is operator & between them.
  2. It doesn't have another operator before { /* statements... */ }, so the comment ends with it.

Finally, this restriction (change) is relaxed by this new rule:

So, in a nutshell, the difference between this suggestion and the proposal from README.md are:

  1. Return types should be inside "" (or any other bracket). (NOTE 1)

But these restrictions (changes) are still required:

So, in a nutshell, the difference between this suggestion and the proposal from README.md are:

  • Template arguments and parameters must be after : instead of ::. (NOTE 2)
  • interfaces, types, ... should be inside :{/*here*/} or any other syntax. (NOTE 1)
  • The syntax of type conversion must be changed from VAR as TYPE to either one of:
    • VAR :as TYPE, if it ends with , or ;.
    • VAR :(as TYPE)
    • (VAR: TYPE) (similar to Flow syntax)
    • or any other syntax. (NOTE 1)
msadeqhe commented 1 year ago

I think I've to clear the rules to make it simple. So in a nutshell, the grammar is like this:

With comment Without comment What was the comment?
(code:cmntgrp) (code) :cmntgrp
[code:cmntgrp] [code] :cmntgrp
{code:cmntgrp} {code} :cmntgrp
code:cmntgrp,code code,code :cmntgrp
code:cmntgrp;code code;code :cmntgrp
code:cmntgrp=code code=code :cmntgrp
code:cmntgrp{code} code{code} :cmntgrp

In which:

The rules are valid only outside object literal syntax (e.g. {var:value} or {key:value}).

Additionally the following part of the proposal from README.md can also be included:

In this way, most of the written code in current TypeScript, Flow, etc will be supported by JavaScript.

theScottyJam commented 1 year ago

This looks really great! Thank you for your time and effort into looking into the plausibility of a route like this. I'm especially impressed that you're working to solve lots of edge cases (such as the return type of functions) that often gets forgotten.

Some questions/thoughts:

How would you handle syntax like the following:

Would there be special syntax provided to help with these? Some of these could easily be done using the : syntax with only minor deviation from TS, such as class MyClass: implements MyInterface { ... } and maybe class MyClass { (:readonly) x: number }?

You said in an earlier comment the following:

keyword (ARGS) => TYPE (keyword can be a meaningful name such as function).

Why is the keyword required? I was understanding that the mere presence of => would extend the comment further.

I'm noticing that one of the things you rely on is an explicit semicolon to end a type comment, like with let a: b;. This does add the ASI hazard where if you forget a semicolon, the following line would accidentally get included as part of the comment. This is sort of a variation of the "token soup" problem that was explained in an earlier TC39 conference, which any "flexible-syntax" proposal will likely be susceptible to. It also wreaks havoc on those who like to code without semicolons (though I personally think that's fine, some of the other upcoming proposals will also be making semicolon-less coding more difficult than what it has been in the past). I'm mostly just pointing this out, not really expecting it to be solved.

I guess one exception to this generic code: comment pattern would be if it conflicts with the label: statement syntax. i.e. maybe if the "code" part is the first part of a statement, and if an identifier, then it will be interpreted as label: statement instead of code: comment?

msadeqhe commented 1 year ago

Thanks for your review/thoughts.

How would you handle syntax like the following:

  • The import type syntax (e.g. import type ... from ... or import { type A, B } from ...)
  • Modifiers like readonly and protected, e.g. class MyClass { readonly x: number }
  • "implements", e.g. class MyClass implements MyInterface { ... }

Would there be special syntax provided to help with these? Some of these could easily be done using the : syntax with only minor deviation from TS, such as class MyClass: implements MyInterface { ... } and maybe class MyClass { (:readonly) x: number }?

Yes, to keep the : syntax with only minor deviation from TS:

The difference between (:comment) and :(comment) is that if we remove the comments, the first one is equal to (), but the second one is equal to nothing.

You said in an earlier comment the following:

keyword (ARGS) => TYPE (keyword can be a meaningful name such as function).

Why is the keyword required? I was understanding that the mere presence of => would extend the comment further.

Because at that moment, I didn't think about a rule to extend the comment further if there is an operator.

I'm noticing that one of the things you rely on is an explicit semicolon to end a type comment, like with let a: b;. This does add the ASI hazard where if you forget a semicolon, the following line would accidentally get included as part of the comment. This is sort of a variation of the "token soup" problem that was explained in an earlier TC39 conference, which any "flexible-syntax" proposal will likely be susceptible to. It also wreaks havoc on those who like to code without semicolons (though I personally think that's fine, some of the other upcoming proposals will also be making semicolon-less coding more difficult than what it has been in the past). I'm mostly just pointing this out, not really expecting it to be solved.

Good point. So if a statement has :-style comment in any part of it, it must end with semicolon ;, otherwise it would be a syntax error. I should add the rule that semicolon ; is required for a statement if it contains :-style comment in itself.

I guess one exception to this generic code: comment pattern would be if it conflicts with the label: statement syntax. i.e. maybe if the "code" part is the first part of a statement, and if an identifier, then it will be interpreted as label: statement instead of code: comment?

That's a good solution. So code: something would be label: statement if its code part is an identifier at the start of the statement, and if it's placed outside class declarations and object literals.

Thanks a lot. I'll update my suggestion to include these notes.

msadeqhe commented 1 year ago

Also :-style comment is not valid within Ternary Operator ?:.

So in general, :-style comments are not valid inside:

But if :-style comments are written in inner expressions, they can be valid inside:

In TypeScript, Flow, etc their syntax is in a way that we can distinguish the comment and the statement within ternary operators and label statements.

theScottyJam commented 1 year ago

Good point. So if a statement has :-style comment in any part of it, it must end with semicolon ;, otherwise it would be a syntax error. I should add the rule that semicolon ; is required for a statement if it contains :-style comment in itself.

I don't feel like that really solves the problem. Take this as an example:

let x: number // <-- oops, forgot a semicolon
x = f(2);

Because JavaScript is (generally) not sensitive to newlines, the above could also be written as the following, and it should mean the same:

let x: number x = f(2);

Which passes your check - it's a statement that ends in a semicolon. It's valid syntax as well. If we strip out all of the comments, we'd be left with this:

let x;

Basically, the fact that we forgot a semicolon has caused it to eat the entire next line without us noticing, which would be fun to debug.

Maybe there's some stuff that can be done to help avoid this issue. While JavaScript is generally not sensitive to new lines, its grammar does contain special rules saying "you are not allowed to place a new line here". Maybe that could be done here to some extent. For example, we could say that, unless a comment is inside a bracket pair, it's not allowed to contain new lines. Thus, this would be a syntax error (either that, or it'll automatically add a semicolon for you, or something):

let x: number // <-- oops, forgot a semicolon
x = f(2);

But spreading a type definition over new lines inside a bracket pair like the following would still be allowed:

let x: {
  a: number
};

// or this should work too
let x: (
  number |
  string
);
msadeqhe commented 1 year ago

Because JavaScript is (generally) not sensitive to newlines, the above could also be written as the following, and it should mean the same:

let x: number x = f(2);

Which passes your check - it's a statement that ends in a semicolon. It's valid syntax as well. If we strip out all of the comments, we'd be left with this:

let x;

Infact if we strip out all of the comments:

let x = f(2);

Because the comment ends with =.

Thanks for the explanation about how new-line within the comment should not be allowed.

spenserblack commented 1 year ago

The question is which one of these two would the following be without types:

let x: number
x = f(2);
  1. let x;
    x = f(2);
  2. let x x = f(2);

    In other words, should the newline mark the end of a type? Should the presence of a following identity mark the end of a type (this AFAIK is going to require one to define a type syntax instead of just ignoring them completely)?

msadeqhe commented 1 year ago

In other words, should the newline mark the end of a type? Should the presence of a following identity mark the end of a type (this AFAIK is going to require one to define a type syntax instead of just ignoring them completely)?

The newline should always mark the end of a type or require the programmer to put the semicolon at the end (otherwise it would be a syntax error), but it shouldn't be optional. In this way, the grammar of :-style comment would be simple to parse. Also multi-line comments are possible within bracket pair (@theScottyJam's idea is here).

IMO semicolon ; should be required for a statement if it contains :-style comment in itself, because if the comment ends with newline, a programmer may mistakenly think that the comment can be extended to the next line.

trusktr commented 1 year ago

Is there a single line type comment? Maybe it could be useful for shorthands, and for minification (ship a library, but perhaps the types are still available, minified on a single line):

// Top level
: type Foo = { whatever } & whatever 

function foo() {
  // Top level of a block
  : interface Foo { whatever }
}
trusktr commented 1 year ago

IMO semicolon ; should be required for a statement if it contains :-style comment in itself, because if the comment ends with newline,

What do you mean exactly? Can you provide an example?

msadeqhe commented 1 year ago

Is there a single line type comment?

No. Thanks for your idea, it would be more pleasant than :{comment}. We may use :: to have statement-like comments. In your example:

// Top level
: type Foo = { whatever } & whatever 

function foo() {
  // Top level of a block
  : interface Foo { whatever }
}

It would be equal to this one without :-style comments (by current rules):

// Top level
= { whatever } & whatever 

function foo() {
  // Top level of a block
  { whatever }
}

Because :-style comments would end with either ,, ;, = or {. That's why we can have :-style comments for variable and function declarations:

let x: comment = value;

function x(): comment { /*...*/ }

Back to the first example, if we use another symbol like :: with different rule-set, we may write that example like this:

// Top level
:: type Foo = { whatever } & whatever 

function foo() {
  // Top level of a block
  :: interface Foo { whatever }
}

:: has a different rule-set. It doesn't end with ,, = or {, so it can be used to make the whole statement as a comment. But still ; would end the comment.

Unlike :-style comments, ; may be optional to write at the end of the statement in ::-style comments. So the rule to find out when the comment ends after ::, follows the rule of bracket pair (we can write newline inside bracket pair, but newline will end the comment outside bracket pair).

msadeqhe commented 1 year ago

What do you mean exactly? Can you provide an example?

If :-style comment ends with newline. The programmer may think it's possible to extend the comment to multiple lines:

let x: comment = call(): comment +
    comment;

But it would be equal to the following code without comments:

let x = call()
    comment;

That is invalid. comment; should've been removed, but it didn't.

If we make ; to be optional in statements with :-style comments, then more rules have to be specified for them. If you think the rule of allowing newline within bracket pair is enough for optional ;, so the semicolon ; doesn't have to be required.

trusktr commented 1 year ago

The main advantage this has over

is that it reduces characters due to not needing comment-closing characters, even in the middle of code due to special rules it can have that existing-comments can't, f.e.

function foo(f: Foo): Bar {...}

// vs

function foo(f /*: Foo */) /* : Bar */ {...}

// vs

function foo(
  f //: Foo
) /* : Bar */ {...}

// vs

function foo(
  f //: Foo
) { //: Bar
  ...
}

For inside-existing-comment syntax, I like #192's direction more than JSDoc for sure. But something about this is also tempting due to the parsing rules allowing the multiline space without closing characters:

:: import Foo from './Foo' // this is a type import, the `type` keyword is not needed now.
import Bar from './Bar' // actual runtime JavaScript

:: interface Foo {
  // ...
}

let f: Foo = {...};

(The :: comment syntax came from #184).

I'm no grammar expert yet: anything undersirable about this from a grammar/implementation perspective?


One thing that is nice about #192 is that it (so far, but maybe in too much of a TS-specific way, but maybe that can be adjusted) allows specifically for documentation description space.

Let's try to see what types and documentation look like with a hybrid of the above type comment syntax and existing-comment syntax from #192. Here's just one example:

:: interface Foo {
  //: description for n
  n: number
}

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

@msadeqhe What happens with multi-line type import? Is there a possible rule for it without actual import syntax being part of the proposal?

:: import {
  Foo,
  Bar,
} from 'somewhere'

EDIT: Hmmm, maybe :: import should be officially part of the proposal because JS languages should not deviate from this! Make a special import syntax that is ignored at runtime?

And for hybrid, something like so?

import { :Type, RuntimeValue } from 'somewhere'
import {
  :Type,
  RuntimeValue
} from 'somewhere'
msadeqhe commented 1 year ago

@msadeqhe What happens with multi-line type import? Is there a possible rule for it without actual import syntax being part of the proposal?

:: import {
  Foo,
  Bar,
} from 'somewhere'

If Foo is an interface type from TS and Bar is a regular JS class, we have to use :-style comment if we want to keep the rules as simple as possible:

import {
  Foo: type,
  Bar,
} from 'somewhere'

Therefore we have to explicitly write type.

If both Foo and Bar are a TS thing, we can use code:comment{code} syntax in this case:

import: type {
  Foo,
  Bar,
} from 'somewhere'

EDIT: sry, we cannot, because it would end up with: import from 'somewhere'. But if JS could support import nothing, we could use it. So the only option is :: import as your suggestion.

or simply ::-style comment is enough as you write it:

...

:: import {
  Foo,
  Bar,
} from 'somewhere'

Hmmm, maybe :: import should be officially part of the proposal because JS languages should not deviate from this!

trusktr commented 1 year ago

or simply ::-style comment is enough as you write it:

...

:: import {
  Foo,
  Bar,
} from 'somewhere'

What's the rule for where the comment ends? It will not end after the }? That's why I thought maybe the whole :: import {} from '...' statement would be an unique comment specifically for official module syntax.

For that matter, we could also just adopt import type and import {type ...} from TypeScript, which would also make it official module syntax, because I believe that we do not want to allow all type systems arbitrarily define their own module syntax.

msadeqhe commented 1 year ago

:: import {} more code would end with either ; or newline (of course if they are not inside inner bracket (quote) pair).

For import {type A, ...} from TS, we can use syntax import {A: type, ...}. For import type {...} if we want to avoid to use ::-style comment, if JS supports import from something which means import nothing from a module, then we can use syntax import from something: type (...) (which is equal to import from something without the comment part) or even import: type (...) (which is equal to import without the comment part).

EDIT: @trusktr, I've fixed the syntax to use (...) instead of {...}.

msadeqhe commented 1 year ago

In a nutshell the rules are:

In all cases:

For example:

msadeqhe commented 1 year ago

In a nutshell the rules are:

  • Statement Comment (:: comment):

    • :: must be stand alone, so only ; or white-space can be before ::.
    • The comment will end before ; or newline.
  • Expression Comment (code :comment separator code):

    • The comment may not start with an opening bracket (quote) (either (, [, {, <, " or ').
    • The comment will end before ,, ;, = or { (as separator).
    • The comment will end before ) (as separator) if it's enclosed within ().
    • The comment will end before ] (as separator) if it's enclosed within [].
    • The comment will end before } (as separator) if it's enclosed within {}.
  • Enclosed Expression Comment (code :comment code):

    • The comment may start with an opening bracket (quote) (either (, [, {, <, " or ').
    • The comment will end with the matching closing bracket (quote) (correspondingly either ), ], }, >, " or ').
    • The end of nested expression comment may be extended with a valid JS operator until it ends with the rules of Expression Comment.

In all cases:

  • : doesn't start a comment within Object Literals, Ternary Operators and Label Statements.
  • If the comment contains a nested bracket pair, it won't end in the middle of the nested bracket pair.

Let's add another comment category named Declaration Comments. They are enclosed within <>, and they are only allowed in declarations. In JavaScript, it's already an error to write <> in declarations, so it's safe to be added to JS syntax. By this rule, we would be able to write generic arguments in every class, function, etc declaration without extra : symbol uniformly:

class A<T> { /*...*/ }
function B<T>() { /*...*/ }
:: interface C<T> { /*...*/ }
:: type D<T> = /*...*/

But for instantiation and generic parameters (invocation) we always use :<> syntax:

let x = new A:<number>();
let y = B:<number>();

// Consider we need an extra nested bracket pair to do this within object literals.
// With comment
let z = { a: (new A:<number>()), b: (B:<number>()) };
// Without comment
let z = { a: (new A         ()), b: (B         ()) };

I've added this new comment category to the rules.

msadeqhe commented 1 year ago

This is an example, what it would look like:

import: type (InterfaceX, TypeAlias) { ClassA, ClassB } from 'somewhere'

:: interface Abc<T> {
    name(): string;
    [i: T]: number;
}

function check<T>(a: { x: number , y: number } & { z: number }): classA<T> | undefined {
    // statements...
}

class Base<T> {
    constructor(width: T, height: T) {
        // statements...
    }

    // declarations...
}

class X<T> extends Base<T>: implements Abc<T> {
    @readonly count: number = 10;

    id(): string {
        return (count: as string);
    }

    // declarations...
}

// This part is not yet a feature of TS, Flow, etc.
let Pi<T> = (3.141592654: T);
let p = Pi:<number>;

let o = new Base:<number>(10, 2);
let u = (10: satisfies number);

Without comments:

import { ClassA, ClassB } from 'somewhere'

function check(a) {
    // statements...
}

class Base {
    constructor(width, height) {
        // statements...
    }

    // declarations...
}

class X extends Base {
    @readonly count = 10;

    id() {
        return (count);
    }

    // declarations...
}

// This part is not yet a feature of TS, Flow, etc.
let Pi = (3.141592654);
let p = Pi;

let o = new Base(10, 2);
let u = (10);
trusktr commented 1 year ago

The import feels a little awkward. What if

import theDefault, {foo, bar}: comment from "somewhere" 

// f.e. all the following are the same, the differing part is only type comment:
import theDefault, {foo, bar}: {One, Two} from "somewhere"
import theDefault, {foo, bar}: type {One, Two} from "somewhere"
import theDefault, {foo, bar}: {type One, type Two} from "somewhere"

// with named imports only
import {foo, bar}: this-is (a) {type} <comment> ! from "somewhere"

// with default import only
import theDefault: blah blah from "somewhere"

// side-effect import only
import: blah blah "somewhere"

As a potential end user of this possible feature, the direction is interesting! But I'm not knowledgeable in parsers/grammars yet. Does this keep the grammar within LR(1) or no?

msadeqhe commented 1 year ago

With the current rules, your suggested import syntax would be like this wihtout comments:

import theDefault, {foo, bar}

// f.e. all the following are the same, the differing part is only type comment:
import theDefault, {foo, bar} from "somewhere"
import theDefault, {foo, bar} {One, Two} from "somewhere"
import theDefault, {foo, bar} from "somewhere"

// with named imports only
import {foo, bar} {type} <comment> ! from "somewhere"

// with default import only
import theDefault

// side-effect import only
import

Does this keep the grammar within LR(1) or no?

I don't have enough knowledge about compilers/transpilers/..., but from what I've researched, it seems it can keep the grammar within LR(1).

azder commented 1 year ago

If I can get one take away of all of this accounting for any and every kind of edge case is that it convinced me even more that inline type comments/declarations is a bad idea.

And this was something I tried to address by https://github.com/tc39/proposal-type-annotations/issues/176 by asking for them to be as a sort of C header declaration or better a Haskell like type definition that will be followed by implementation free of anything of the sort.

Wouldn't it be easier for compilers because it would provide for simpler syntax? I know that I personally find it easier for my human eyes and my human (non-AI) mind to reason about.

Just raising it as a concern here and wouldn't want to comment it more here as to not detract from this issue subject

msadeqhe commented 1 year ago

Thanks. I'll continue this subject (using regular comments instead of :-style comments) in #176.

trusktr commented 1 year ago

With the current rules, your suggested import syntax would be like this wihtout comments:

I was thinking that because import syntax is standard, comments like that are ok up until the string, as a rule. I don't think anyone needs a string inside the import type comment, but they could with :()

Lookwe69 commented 8 months ago

Why not opt for a syntax which can be parsed more easily ? Instead of :, i propose :: by default. With this syntax, we can add type where we currently cannot with typescript. For exemple with object destructuring;

E.g function myFunction(firstParam:: string, {optionalPram1:: boolean = true, optionalParam2:: string | null = null, optionalParam3: param3:: number = 0} = {}):: boolean { .... }

Instead of just copy what is existing, i think it's important to fix what's blocking in the current syntax.

I think : is overuses in JS to be usable in every situation