[Feature Request] Proposal for type annotations as comments
See original GitHub issueProblem
- There are many authors that want to use TypeScript without a build step
- JSDoc is a verbose substitute for TypeScript and doesn’t support all TS features.
- 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:
- This wouldn’t be a breaking change in existing TypeScript/JavaScript code
- This wouldn’t change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript’s Design Goals.
⭐ Proposal
A JavaScript file would be preceded by
// @ts
The reason this is needed is to indicate the author’s intent at
- Type-checking this JavaScript file as adhering to all of TypeScript’s restraints (the TSConfig file), so a stronger version of
@ts-check
. - 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 ofstatement
in TypeScript, and used to mark completetype
/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?
- The TC39 proposal is more limited than this proposal, for syntax space reasons
- 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?
- 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).
- 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.
- 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.
Issue Analytics
- State:
- Created a year ago
- Reactions:13
- Comments:30 (2 by maintainers)
Top GitHub Comments
Lets be more specific with examples here.
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.
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 :
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).
Another concise comemnt suggested in that proposal is: