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

Trojan Horse Concerns #187

Open spillz opened 1 year ago

spillz commented 1 year ago

The title is dramatic but it is at least a concise expression of how I feel about a proposal that adds an enormous amount of syntactic complexity and expressive overhead to the language with what is intended to be no runtime implications. I too value the benefits that type annotations can provide but I believe the annotation approach in this proposal suffers from a few major flaws relative to simpler annotation approaches:

  1. The proposal as specified, even if labeled strawman, clearly furthers the commercial interest of one private vendor, whether or not the parties advancing the proposal have any affiliation.
  2. The strongest use case for type annotation is for library code APIs by helping library users understand the API, reducing user errors significantly, and speeding user code development. But once a library maintainer lets that camel's nose in the tent it becomes a battle to stop it from infecting internal library code as well, taking a library from type annotations in the API to type annotations everywhere. There are huge tradeoffs in terms of time to refactor code and type brittleness of TypeScript that make types everywhere a bad idea in a dynamically typed language. Recent announcements from maintainers of prominent libraries abandoning TypeScript within their core library (or altogether) make this point.
  3. Adding these annotations is literally adding extra payload to HTML and JS files and increasing latency. This also raises the potential of securities issues, especially given that parsing out what part of the syntax is optional type info isn't always straightforward given how expressive TypeScript typing is. I'm mindful that these annotations will be added to code that could potentially end up running on browsers all over the world. That's not to say there aren't security issues in other language features but this just seems to be expanding the surface area of vulnerabilities for limited benefit relative to simpler type annotation approaches (see below).
  4. The resemblance of the proposed syntax to the syntax of statically typed languages, i.e., interweaved on the same line as functional code itself, is a cognitive trojan horse. It potentially traps developers, especially new developers who might see annotations presented as best practice, into a mentality that dynamic typing is always bad or that explicit is always better than implicit. One of the benefits of using a dynamically typed language on the web is that we can break gracefully.

Given the above, I don't believe the alternative of a JSDoc-ish syntax that would cleanly delineate annotations from functional code is being taken seriously enough in the proposal. By "JSDoc-ish" I don't mean that the types should literally be embedded in comments but, for example, a type annotation marker (for single lines perhaps "@", "#" or "~") could be added to the language that is functionally equivalent and as easy to parse as a comment at runtime but also easily distinguishable from an actual comment. That at least means you could not interweave annotations and code on the same line as proposed without actually opening and closing the annotation (e.g., /@, @/) or restricting annotations to the end of a line (a la // comments). My preference would be for annotations to always be on separate lines (or separate blocks of lines to longer defs) from functional code each demarked by a single leading character, which would then make them extremely easy to strip at runtime and easily distinguished by developers. The type annotation syntax itself should still be part of the ES spec, which I believe would be important for adoption and to avoid fragmentation. A motivation to include the syntax in the spec is that the annotations could also be used for inference inside of browser debugging tools. I've seen examples of one liner annotations prefixing lines of functional code such as methods and functions given in other Issues here that look as readable as the TypeScript version even without IDE UI hints, especially if you replaced the comment marker with another character.

If you got this far, thanks for considering an alternative viewpoint!

spillz commented 1 year ago

Re assertions, VS Code does a nice job of flagging invalid ones at least, which can save some refactor bugs: jsdoc-assert

Also, the VS Code ergonomics for type annotating JSDoc's for existing code are pretty decent too. Just tab/type JSDoc-annotate

And provides completion hints etc. just like TS: JSDoc-hints

somebody1234 commented 1 year ago

yeah, well... the language service provider for jsdoc (and javascript in general) in vscode is typescript, after all

trusktr commented 1 year ago

There is a common misconception that there is an intrinsic need for an extra pair of parenthesis

@lillallol according to that issue you linked they are required. That's all I was saying.

trusktr commented 1 year ago

The abstract class gets compiled to an empty class. I am just trying to understand why would anyone extend an empty class, so that I can make a correct conversion?

@lillallol

Abstract classes can have non-abstract members that get inherited. I ended up figuring it out here: https://github.com/microsoft/TypeScript/issues/48650#issuecomment-1730818001 I was trying to use `.js`+`.d.ts` declaration initially. Instead, I realized to just use `.js` with JSDoc (and sometimes `.ts` to import types), _without declaration files_. When trying with declaration files, it required duplicating across `.js` and `.d.ts`, and would not type check the implementation, and that's where I was not having a good time.
lillallol commented 1 year ago

@trusktr

Regarding the example you have posted link it all boils down to separating the implementation and the type of an abstract class. Unfortunately TypeScript does not provide such a feature (yet). It is my understanding that this a matter of support and not something that intrinsically can not be supported. Here is how I would do it if such a feature was supported:

export class Bar {bar = 123};

/**@type {import("./index").IFoo}*/
export const Foo = class {
    //This method will not be needed if TypeScript support abstract member in
    // IFoo
    method() {
        throw Error();
    }
    realMethod() {
        const ret = this.method()
        console.log('do something with subclass value', ret)
        return ret
    }
}

/**@type {import("./index").IFooBar}*/
const FooBar = Foo;

class Test extends FooBar {
    method() { return new Bar() } // ok
}

class Test2 extends FooBar {
    method() { return new Object() } // type error
}

new FooBar() // type error

const b = new Test().realMethod();
// does not type error because abstract member in IFoo is not supported yet by
// TypeScript, although it will throw error in runtime
(new Test()).method()
import { Bar, Foo } from "./test";

export type IFoo = abstract new <T>() => {
  //It all boils down to TypeScript supporting abstract members
  abstract method() : T 
  realMethod() : T
};

export type IFooBar = typeof Foo<Bar>;