TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

[Feature Request] Proposal for type annotations as comments

Open matthew-dean opened this issue 3 years ago • 66 comments

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 https://github.com/microsoft/TypeScript/issues/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:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • [x] 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 avatar Apr 12 '22 02:04 matthew-dean

Another concise comemnt suggested in that proposal is:

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

Jack-Works avatar Apr 12 '22 02:04 Jack-Works

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.

lillallol avatar Apr 12 '22 07:04 lillallol

@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 avatar Apr 12 '22 14:04 matthew-dean

@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 avatar Apr 12 '22 14:04 matthew-dean

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

matthew-dean avatar Apr 12 '22 14:04 matthew-dean

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

lillallol avatar Apr 12 '22 15:04 lillallol

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

matthew-dean avatar Apr 12 '22 17:04 matthew-dean

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

lillallol avatar Apr 13 '22 05:04 lillallol

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.

simonbuchan avatar Apr 13 '22 10:04 simonbuchan

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

dfabulich avatar Apr 14 '22 05:04 dfabulich

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

simonbuchan avatar Apr 14 '22 06:04 simonbuchan

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

dfabulich avatar Apr 14 '22 16:04 dfabulich

@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 avatar Apr 14 '22 17:04 matthew-dean

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

matthew-dean avatar Apr 14 '22 18:04 matthew-dean

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)

somebody1234 avatar Apr 14 '22 19:04 somebody1234

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

matthew-dean avatar Apr 14 '22 22:04 matthew-dean

(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/::<T>/ { a/: T /; b/: [T] /; c/::<U>/() {} d/::?<U>/() {} } class ClassModifier { /:: private/ bar/: string/ } let letGeneric1/: ClassGeneric<1> /; let letGeneric2/:: !: ClassGeneric<1> /; function fnGeneric/::<T>/(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 = /::<T>/() => {}; const genericAsyncArrowFunction = async /::<T>/() => {}; // note that these two are invalid // const genericSimpleArrowFunction = /::<T>/ foo => {}; // const genericSimpleAsyncArrowFunction = async /::<T>/ foo => {}; const prefixTypeAssertion = /::/ 1;

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

somebody1234 avatar Apr 15 '22 03:04 somebody1234

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

Jack-Works avatar Apr 15 '22 03:04 Jack-Works

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 avatar Apr 15 '22 04:04 somebody1234

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

somebody1234 avatar Apr 15 '22 04:04 somebody1234

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.

simonbuchan avatar Apr 15 '22 06:04 simonbuchan

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

somebody1234 avatar Apr 15 '22 06:04 somebody1234

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.

simonbuchan avatar Apr 15 '22 08:04 simonbuchan

But again, I've only spent like an hour on this!

simonbuchan avatar Apr 15 '22 08:04 simonbuchan

find and replace as in, replacing /*:: with three spaces; */ with two. which should break exactly zero location reporting

treat /*:: and a matching */ as separate comment trivia

That was, in fact, my initial approach, but it's just (IMO) awful for checking that, y'know:

  • things that should be in type comments are actually in type comments
  • things that shouldn't, are actually not in type comments
  • type comments pair up in reasonable places - i.e. no /*:: class Foo */
  • type comments are balanced at all etc etc

In other words - AFAICT it'd make parsing almost trivial, but getting diagnostics at all, let alone correct ones, will be much, much harder

as for "have some code run 100x worse" - i don't think so here. but there's like, say, up 2x the work done in certain parts so there's potential of, say, 10% (or more?!) worse performance since everywhere it checks for a type annotation it must also check for /*: and /*:: now

somebody1234 avatar Apr 15 '22 08:04 somebody1234

I'm especially keen on

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

as far as I know there's no way to do this with JSDoc at the moment.

jespertheend avatar May 08 '22 15:05 jespertheend

@jespertheend

as far as I know there's no way to do this with JSDoc at the moment.

Sorry but I do not fully understand what you are trying to do with the example you provide. Can you provide a type definition for add and Point?

lillallol avatar May 08 '22 15:05 lillallol

The example was taken from the first comment. Basically in TypeScript when a function has a generic parameter:

function add<T>(a: T, b: T) : T {
  // do something
  return {} as T;
}

You can simply invoke it like

add<number>(2, 3);

With JavaScript and JSDoc this is not possible.

Normally this is not a big deal since the generic parameter will be implicitly inferred by the types you pass in for a and b, but I'm running into an issue right now where I want to make sure my types are correct for the assertEquals(actual, expected) function in deno_std. But the function has been overloaded with an extra signature that doesn't contain the generic parameter. So if you want to make sure the values you pass in for actual and expected have the same type, you have to either use TypeScript or write a wrapper function for assertEquals().

jespertheend avatar May 08 '22 15:05 jespertheend

With JavaScript and JSDoc this is not possible.

This is a common misconception that is not valid. Here is how to do it.

For some strange reason I am not getting the errors :

  • Variable 'token1' is used before being assigned.
  • Variable 'token2' is used before being assigned.

in the playground (I am too lazy to find why) although I do get them in vscode.

I think it is a matter of support from the types system to enable a built in type function (e.g. token<> and untoken<>) to make clear our intentions to the type system so such errors not occur.

Edit : By the way you provided the solution your self, without the extra parameter solution I provided.

lillallol avatar May 08 '22 16:05 lillallol

seems like its because strictNullChecks is off by default in playground js: Playground

somebody1234 avatar May 09 '22 03:05 somebody1234