Open matthew-dean opened 2 years ago
Another concise comemnt suggested in that proposal is:
//:: TypeA, TypeB => ReturnType
function f(a, b) {
return a.handle(b)
}
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.
@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).
@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.
@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
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 apitestTypes.ts
: self explanatoryI 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
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.
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).
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.
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 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!
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;
}
@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
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.@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".
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 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?
(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)
/*:: */
(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
/** jsdoc */ /*:: type T = Foo; */
/*
to be parsed as slash asterisk since it is technically already in a comment, so they cannot start another one
*/
correctly/*:
(and */
when not ending a block comment) are scanned as tokens rather than trivia (comments)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
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)/*: T */
and /*:: : T */
(anywhere that accepts /*: */
also accepts the other, in general)
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 knowimport { /*:: type Foo */ }
(/*:: import type {} */
is allowed as `/:: statement1; /)/*:: statement1; statement2; */
/*:: classMember1; classMember2; */
(index signatures and declared members are allowed)/*:: as T */
, /*:: as const */
asConst()
, implemented using a variant of the wonderful Narrow<T>
typefunction 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./*::<T>*/ () => {}
foo/*::!*/
class Foo { bar()/*::?*/ {} }
but on second thought, is that even useful???
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 finesrc/compiler/parser.ts
and do a search for setInTypeCommentContextAnd
function foo();
/*:: type Left = 1; type Right = 2; type Test = Left | Right; type Test2 = Left | Right; let invalid: Test; */ type Invalid = 1; let annotation/*: string*/ = 'a'; /* normal comments should work fine */ export function fromString(s/*: string */) { // } export function toString() { // } let foo/*: number */; const testNestedAsConst = { arr: [1, 2, 3] /*:: as const */ }; const TokenType = { CRLF: 0, } /*:: as const */; /*:: type TokenType = (typeof TokenType)[keyof typeof TokenType]; type Node = | { type: typeof TokenType.CRLF; }; */ const asConstTest = 1 /*:: as const */; function argsTest(s/*:: : string */, s2/*:: ?: string */) { return 0; } function thisParamTest(/*:: this: number, */ a/*: string */) { return 0; } function thisParamTest2(/*:: this: number*/ a/*: string */) { return 0; } function thisParamTest3(/*:: this: number */) { return 0; } let foo1 /*: number */; let testTheThing/*: number */; // let testTheThing2/*:: : */ // let fn = fnGeneric/*::*/('ok'); class ClassIndex1 { /*::[k: string]: string;*/ } class ClassMember1 { a/*: string */ } class ClassMember2 { a/*: string */; } class ClassMember3 { a/*:: : string */ } class ClassMember4 { a/*:: : string */; } // let a: ; // let a /*: */; class ClassMember5 { a/*:: ?: string */ } class ClassMember6 { a/*:: ?: string */; } class ClassMember7 { a/*:: !: string */ } class ClassMember8 { a/*:: !: string */; } class ClassMembers1 { a/*: string */ b/*: string */; c/*: string */ } class ClassMembers2 { /*::[k: string]: string;*/ a/*: string */; b/*: string */ c/*: string */; } class ClassDeclare1 { /*:: [k: string]: string; declare a: '1'; declare b: '2'; c: '3'; */ } class ClassGet1 { get a() { return 1; } set a(v) {} static get a() { return 1; } static b = 1; // /*:: static */ a = 1; private get c() { return 1; } /*:: private */ get d() { return 1; } /*:: public */ static set e(v/*: number */) {} } let classGet1/*:: !: ClassGet1 */; classGet1.d; class ClassModifiers1 { /*:: public */a/*: number */; } class ClassModifiersInvalid { /*:: public static*/ public a/*: number */; } class ClassMisc { bar/*::?*/()/*: number*/ { return 0; } get foo(): number { return 0; } } class ClassPropertyConstructor { constructor(/*:: public */a/*: number */) { // } } const what = { foo!: 1 }; /*:: export = asConstTest; interface I {} type Wot = { private a: number; // private b/*: number*/; // /*:: private */a/*: number */; }; declare function declareTest(): void; */ declare function declareTestBad()/*: void*/; /*:: declare */ let a: number; // TODO: make sure all typescript syntax still correctly errors in js class ClassExtendsImplements extends String /*:: implements I */ { } class ClassImplements /*:: implements I */ {} class ClassGeneric/*:: */ { a/*: T */; b/*: [T] */; c/*::*/() {} d/*::?*/() {} } class ClassModifier { /*:: private*/ bar/*: string*/ } let letGeneric1/*: ClassGeneric<1> */; let letGeneric2/*:: !: ClassGeneric<1> */; function fnGeneric/*:: */(it/*:T*/) { return it; } /*:: declare module "./types_as_comments_test" { type A = 'A'; type B = 'B'; type C = 'C'; type D = 'D'; type E = 'E'; type F = 'F'; } */ import { /*:: type A,*/ } from "./types_as_comments_test" import { B, /*:: type C,*/ } from "./types_as_comments_test" import { D, /*::type E, type F */ } from "./types_as_comments_test" export { /*:: type A as A_*/ } export { B as B_, /*::type C as C_*/ } export { D as D_, /*::type E as E_, type F as F_ */ } function thisParamIdk(/*:: this: Foo3 */) {} class Foo3 { /*::declare x: Foo3;*/ // TODO: this errors // /*::declare y: Foo3;;*/ } class Foo4 { x/*: Foo4 */ } fnGeneric/*::<[1, String, () => {}, (a: 1, ...b: any[]) => asserts a is any, (a:number)=>a is 1, number, {[k:1]:2;}]>*/(); // const instantiationExpression = [].map/*:: */; const nonNullAssertion = 1/*::!*/; const genericArrowFunction = /*:: */() => {}; const genericAsyncArrowFunction = async /*:: */() => {}; // note that these two are invalid // const genericSimpleArrowFunction = /*:: */ foo => {}; // const genericSimpleAsyncArrowFunction = async /*:: */ foo => {}; const prefixTypeAssertion = /*:: */ 1; /*:: declare interface AAAAA { bar: 1; } */
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')
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
)
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
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.
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
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.
But again, I've only spent like an hour on this!
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:
/*:: class Foo */
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
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
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
?
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()
.
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 :
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.
seems like its because strictNullChecks is off by default in playground js: Playground
@lillallol the extra variables in your JSDoc example for @jespertheend are very superfluous. We definitely need the concise syntax.
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.
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).
I recently learned that JSDoc syntax already has a somewhat more concise form.
Instead of this:
/**
*
* @param {number} a
* @param {number} b
* @param {number} c
* @returns number
*/
function method(a, b, c,) {
return a + b + c;
}
You can write this:
/** @type {(a: number, b: number, c: number) => number} */
function method(a, b, c,) {
return a + b + c;
}
It works today, out of the box with TypeScript.
Here's a more complex example. You can turn this:
/**
* @typedef {Object} MemberGroupsResponse
* @property {Object.<string, MemberGroup>} memberGroups
* @property {Object.<string, Array.<string>>} memberGroupsByUser
*
* @function getMemberGroupsUncached
* @async
* @param {string} month
* @param {string} memberGroupsSheet
* @returns {Promise.<MemberGroupsResponse>}
*/
async function getMemberGroupsUncached(month, memberGroupsSheet) { /* ... */ }
… into this:
/**
* @type {(month: string, memberGroupSheet: string) => Promise<{
* memberGroups: Record<string, MemberGroup>;
* memberGroupsByUser: Record<string, string[]>;
* }>}
*/
async function getMemberGroupsUncached(month, memberGroupsSheet) { /* ... */ }
Still, improvement here is possible. It'd be great if there were a /*: ... */
shorthand for /** @type {...} */
. That'd be especially useful for JSDoc Casts.
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString);
Could be:
/*: number | string */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /*: number */ (numberOrString);
Today, when you want to use TS as const
in JSDoc, you have to do it like this:
let one = /** @type {const} */(1);
It'd be great to do it like this:
let one = /*: const */(1);
You can write this:
/** @type {(a: number, b: number, c: number) => number} */ function method(a, b, c,) { return a + b + c; }
Yes indeed, I am aware of using TS syntax inside JSDoc syntax, and I do take advantage of it, but there's one particular situation where it falls apart:
We cannot add documentation at all when using that format. For example, try converting this one over:
/**
*
* @param {number} a - description for a
* @param {number} b - description for b
* @returns number - description for return value
*/
function method(a, b) {
return a + b;
}
We need a concise syntax for documentation and types.
var typeAssertedNumber = /** @type {number} */ (numberOrString);
var typeAssertedNumber = /*: number */ (numberOrString);
Indeed that one has been annoying, and your idea there is a lot better. Another possibility is:
var typeAssertedNumber = numberOrString /*: as number */;
// or
var typeAssertedNumber = numberOrString //: as number
Is it possible to prototype this comment type system as a TypeScript transform plugin? Ideally the prototype would have:
Yes indeed, I am aware of using TS syntax inside JSDoc syntax, and I do take advantage of it, but there's one particular situation where it falls apart:
We cannot add documentation at all when using that format.
That is not true. You write the type in a ./privateApi.ts
file like this:
/**
* some description about what this function does (that is actually what it
* returns)
*/
export declare function method(
/** description for a*/
a: number,
/** description for b*/
b: number,
) : number;
and then import it in a .js
file like this:
/**
* @type {import("./privateApi.js").method}
*/
function method(a, b) {
return a + b;
}
//hover over method and VSCode will popup a window that have both the types and description
method(,)//the comma will trigger the popup with description for parameters in VSCode
If you attempt to do the same thing, using arrow functions (both in .ts
and .js
), it will not work. It is a matter of support from the typescript team to make it work for arrow functions as well.
doc generator that can extract docs (as from my above examples) and produce basic HTML output as a starting point to be able to transition from JSDoc-based tools for documentation generation.
Notice that we achieved to write both the type and the JSDoc description in a single place, without the implementation. That is all we need for documentation. We do not actually need documentation generation libraries, they are an anti pattern, as it can been seen from the following issue I opened in typedoc [link].
Regarding type as
assertions I do consider them an anti pattern since they override the type checker and can be replaced with run time assertions. Const assertions can be made using functions calls nowadays so no extra syntax is required for them inside JSDoc in .js
files.
I have created projects in which all types are defined in .ts
and are imported in .js
files via /**@type {import("./path/to/file.js")}*/
. The argument of verbosity has never been an actual issue for me.
To sum up I think that there is no need to implement any of the changes suggested in the context issue.
That is not true. You write the type in a
./privateApi.ts
file like this:
That is possible indeed, but I think some people prefer not to split to separate files, which is a valid desire. So my comment was correct, when staying in the same file.
some people prefer not to split to separate files
I can accept that as a valid argument since we are in the typescript repo and not the type annotations proposal repo.
How do you separate the implementation? When the programmer want to use type checking while using a library?
/**
* @type {import("./privateApi.js").method}
*/
function method(a, b) {
let x: number = a + b;
let y: number = a * b;
return x / y;
}
How do you separate the implementation? When the programmer want to use type checking while using a library?
/** * @type {import("./privateApi.js").method} */ function method(a, b) { let x: number = a + b; let y: number = a * b; return x / y; }
If we separate the whole implementation to .ts
file, then we're not using JavaScript.
How do you separate the implementation? When the programmer want to use type checking while using a library?
If we separate the whole implementation to .ts file, then we're not using JavaScript.
I do not understand what you are talking about. Can I see more detailed examples please.
I mean your assumption that "we don't need type annotation in JS because we can put type annotation in separate .ts
file and import it in .js
file", is wrong.
Because in your solution, only type annotation for function declarations can be imported to JS, but most of the times, we are using type annotation in function body. That's why we need to be able to use type annotation in JS files. Of course if we put the function body in .ts
file, we have to transpile it from TS to JS, that's why your solution doesn't work.
only type annotation for function declarations can be imported to JS
That is not valid. You can import any type from .ts
files to annotate statements.
/**
* @type {import("./privateApi.js").IMethod}
*/
function method(a, b) {
/**
* No need to use that annotation anyway since TS infers types.
* @type {import("./privateApi.js").INumber}
*/
let x = a + b;
/**
* No need to use that annotation anyway since TS infers types.
* @type {import("./privateApi.js").INumber}
*/
let y = a * b;
return x / y;
}
//./privateApi.ts
export type IMethod = (a : number, b : number) => number;
export type INumber = number;
Regarding the type annotations proposal the enforcement of separation of implementation and abstractions in different files enables someone to standardise type-import annotations without the need to define a type system and simultaneously not restricting its syntax. That is exactly the proposal I want to present to tc39.
OK. How do you annotate the type of x
and y
variables?
/**
* @type {import("./privateApi.js").IMethod}
*/
function method(a, b) {
/**
* No need to use that annotation anyway since TS infers types.
* @type {import("./privateApi.js").INumber}
*/
let x = a + b;
/**
* No need to use that annotation anyway since TS infers types.
* @type {import("./privateApi.js").INumber}
*/
let y = a * b;
return x / y;
}
This is a simple example, but in complicated function bodies, we need a direct way to use TypeScript features within function bodies.
This is a simple example, but in complicated function bodies, we need a direct way to use TypeScript features within function bodies.
Please give an example.
Here is one. But then again it is a matter of support, i.e. not an intrinsic inability of the way of static typing I suggest.
This is an example from VSCode repository:
public selectToBracket(selectBrackets: boolean): void {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const newSelections: Selection[] = [];
this._editor.getSelections().forEach(selection => {
const position = selection.getStartPosition();
let brackets = model.bracketPairs.matchBracket(position);
if (!brackets) {
brackets = model.bracketPairs.findEnclosingBrackets(position);
if (!brackets) {
const nextBracket = model.bracketPairs.findNextBracket(position);
if (nextBracket && nextBracket.range) {
brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());
}
}
}
let selectFrom: Position | null = null;
let selectTo: Position | null = null;
if (brackets) {
brackets.sort(Range.compareRangesUsingStarts);
const [open, close] = brackets;
selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();
if (close.containsPosition(position)) {
// select backwards if the cursor was on the closing bracket
const tmp = selectFrom;
selectFrom = selectTo;
selectTo = tmp;
}
}
if (selectFrom && selectTo) {
newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
}
});
if (newSelections.length > 0) {
this._editor.setSelections(newSelections);
this._editor.revealRange(newSelections[0]);
}
}
newSelection
, selectFrom
and selectTo
have type annotations. How do you put the declaration of them to a separate .ts
file? How do you import them to .js
file? They are local variables.
What about local functions and local types? If we put all of them to a .ts
file, it would resemble spaghetti coding style.
This is not valid .js
nor .ts
. Can I have minimal reproducible example? Nvm:
/**@type {import("./some/place.js").ISelectToBracket}*/
selectToBracket(selectBrackets) {
if (!this._editor.hasModel()) {
return;
}
const model = this._editor.getModel();
const newSelections: Selection[] = [];
this._editor.getSelections().forEach(selection => {
const position = selection.getStartPosition();
let brackets = model.bracketPairs.matchBracket(position);
if (!brackets) {
brackets = model.bracketPairs.findEnclosingBrackets(position);
if (!brackets) {
const nextBracket = model.bracketPairs.findNextBracket(position);
if (nextBracket && nextBracket.range) {
brackets = model.bracketPairs.matchBracket(nextBracket.range.getStartPosition());
}
}
}
/**@type {import("./from/somewhere.js").IPostionNull}*/
let selectFrom = null;
/**@type {import("./from/somewhere.js").IPostionNull}*/
let selectTo = null;
if (brackets) {
brackets.sort(Range.compareRangesUsingStarts);
const [open, close] = brackets;
selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();
if (close.containsPosition(position)) {
// select backwards if the cursor was on the closing bracket
const tmp = selectFrom;
selectFrom = selectTo;
selectTo = tmp;
}
}
if (selectFrom && selectTo) {
newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
}
});
if (newSelections.length > 0) {
this._editor.setSelections(newSelections);
this._editor.revealRange(newSelections[0]);
}
}
it would resemble spaghetti coding style.
There is no need to use a single file for your types. I usually use these:
publicApi.ts
is used to define the public APIprivateApi.ts
is used to define the private APItypes.ts
is used to define types used by the abstractions and
implementations of the private APItestApi.ts
is used to define types that are used only in test filesI never had a problem with spaghetti code.
@lillallol Here's a simple repo with the first commit written in TypeScript, npm i && npm start
installs typescript, builds to JS, opens a browser tab, logs to console.
https://github.com/trusktr/buildless-typescript
What's the best way (in your opinion) to convert it to buildless (keep only the static server) while keeping the code as simple as possible? Can you make a PR so we can see the diff?
EDIT, nevermind, I updated it to JS with no build. That one wasn't so bad! Better to not have separate files for the classes in that case.
Here's one class:
/**
* @abstract
* @template {object} T
*/
export class Foo {
foo = "456"
/**
* @abstract
* @returns {T}
*/
method() {
throw "subclass must implement"
}
doFoo() { this.foo; this.method() }
}
@abstract
doesn't work yet (feature request). Here's the other class:
import { Bar } from "./Bar.js"
import { Foo } from "./Foo.js"
const FooBar = /** @type {typeof Foo<Bar>} */ (Foo)
export class Test extends FooBar {
/** @override */
method() {
return new Bar()
}
}
const b = new Test().doFoo()
b.logBar()
with terser comments, it could be:
export /*: abstract */ class Foo/*:<T extends object>*/ {
foo = "456"
/*: abstract */method() { //: T
throw "subclass must implement"
}
doFoo() { this.foo; this.method() }
}
import { Bar } from "./Bar.js"
import { Foo } from "./Foo.js"
export class Test extends Foo/*:<Bar>*/ {
/* :override */ method() {
return new Bar()
}
}
const b = new Test().doFoo()
b.logBar()
That still feels a little awkward and noisy to me. Here's an alternative that I like more:
//: abstract
export class Foo { //:<T extends object>
foo = "456"
//: abstract
method() {} //: T
doFoo() { this.foo; this.method() }
}
import { Bar } from "./Bar.js"
import { Foo } from "./Foo.js"
export class Test extends Foo { //:<Bar>
//: override
method() {
return new Bar()
}
}
const b = new Test().doFoo()
b.logBar()
Ideas are varied at the moment, but I think that last one is cleanest so far.
There would need to be rules that associate the comments to the parts they annotate. For example, the //: T
annotates method
's return value because that's the last thing on the same line that it can (say, we're not annotating the curly braces, but the function)
SIde by side:
/**
* @abstract
* @template {object} T
*/
export class Foo {
foo = "456"
/**
* @abstract
* @returns {T}
*/
method() {
throw "subclass must implement"
}
doFoo() { this.foo; this.method() }
}
//: abstract
export class Foo { //:<T extends object>
foo = "456"
//: abstract
method() {} //: T
doFoo() { this.foo; this.method() }
}
I ended up spending some time working on this problem, and was able to put together a fork of TypeScript that allows TypeScript syntax to be inside of comments - it took more work than I thought it would :p. Anyways, it's up one NPM and GitHub.
As for putting JSDocs inside of a "TS Comment" (What I'm calling these /*:: ... */
/*: ... */
things), I ended up solving that issue by just having you close both the JSDoc comment and TS comment with the same closing comment token, then right afterwards you would re-open the TS comment, like this:
/*::
interface User {
readonly username: string
readonly birthday: Date
/** @deprecated *//*::
readonly age: number
}
*/
It's a little funky, but it works good enough for now.
Problem
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:
⭐ Proposal
A JavaScript file would be preceded by
The reason this is needed is to indicate the author's intent at
@ts-check
.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 ofstatement
in TypeScript, and used to mark completetype
/interface
blocks and other types of assertions.//:
and//::
when the type / type import occupies the whole line / remainder of the lineHere's a basic example, borrowed from Flow:
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:
Important Note: an author should not be able to put any content in
/*:: */
blocks. For example, this should be flagged as invalid: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?
What shortcomings exist with this approach?
/*:: */
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.