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

How is this proposal better than Source Maps? #143

Open codedread opened 2 years ago

codedread commented 2 years ago

I think the case for this proposal could be helped if the authors highlighted how this proposal could eventually lead to a better developer story than simply using source maps which already works today, makes developers lives easier (full source with comments), requires no runtime changes, and allows users to ship compiled/minified code.

cpoftea commented 2 years ago

this proposal could eventually lead to a better developer story than simply using source maps

Source maps will be required still to parse compiled/minified code since all types will be striped away, variable names changed and some optimizations applied by bundlers/compilers/transpilers.

codedread commented 2 years ago

Right so we have the following situation today when using a statically-typed, transpiled-to-JS language (like Typescript):

So other than a potentially reduced build step with significant downsides [1], what does this new proposal buy anyone?

[1] These are the ones I find concerning: Developer confusion ("when do I use tsc?", "which browsers support this feature?"), larger JS bundle size (negatively affects the user experience), increased complexity in browser runtimes (negatively affects the user experience).

david0178418 commented 2 years ago

[1] These are the ones I find concerning: Developer confusion ("when do I use tsc?", "which browsers support this feature?"), larger JS bundle size (negatively affects the user experience), increased complexity in browser runtimes (negatively affects the user experience).

With regard to developer confusion and increased runtime complexity, would these not also be true for any feature? There are a number of primitives and syntactic sugars that landed in transpilers well before they were standardized, where their utility was demonstrated and the ideas tinkered with. That's what informs much of the standardization discussions.

As for bundle size, I don't believe this would be an issue for production code as it would be stripped with the minification step.

As for the gains, there is immense value in being able to open and editor write code that more closely resembles what you'd write in your application. This is especially true during the early years of a developer's career, where they can focus on the code itself as opposed to what can be a daunting ecosystem of tools.

lillallol commented 2 years ago

As for the gains, there is immense value in being able to open and editor write code that more closely resembles what you'd write in your application. This is especially true during the early years of a developer's career, where they can focus on the code itself as opposed to what can be a daunting ecosystem of tools.

This is already possible without this proposal.

There are actually no benefits with this proposal. There are only drawbacks. The same is valid when going for super sets.

zsakowitz commented 2 years ago

Sometimes I want to prototype some TS code, but I either have to use JSDoc or start up a terminal before I can check it in my browser. It would be nice if this code:

function first<T>(array: T[]): T {
  return array[1]
}

just ran in the browser without needing to look in my dist directory or using a transpiler, even if it’s just test code that I won’t actually use in production.

lillallol commented 2 years ago

It would be nice if this code: just ran in the browser without needing to look in my dist directory or using a transpiler, even if it’s just test code that I won’t actually use in production.

Well you provided already the solution yourself : JSDoc.

david0178418 commented 2 years ago

Well you provided already the solution yourself : JSDoc.

The proposal has a fairly thorough response to this.

https://github.com/tc39/proposal-type-annotations#limits-of-jsdoc-type-annotations

lillallol commented 2 years ago

@david0178418

The response is by no way thorough. In fact, for some strange reason, it completely omits mentioning JSDoc with TypeScript features, which I have mentioned before[0].

Strangely enough, the same thing happened in the JSConf presentation[1], made by the original author of this proposal.

It is not by chance that on the tc39 meetings [2][3] for stage 1, it was never claimed by the champions that types in comments lack in static type safety when compared to the super sets. Also it is not by chance that tc39 members never took the problem statement of avoiding an extra compilation step, as a real problem, which eventually made the champions change the problem statement.

Unfortunately the README.md of this proposal is outdated since 2 months ago, and has not even be updated with the links of the tc39 meetings.

Going though the tc39 meeting notes, I am left wondering how even with the updated problem statement, tc39 members gave stage 1. It is not EcmaScript that has to change for the bad design choice of TypeScript and Flow to introduce static type checking as super sets, which by the way is the sole reason for:

developer friction

It is the supersets that have to be reduced to complements. What is a complement? SvelteKit[4] is a loose example of what TypeScript as a complement is.

Arguments used by the champions, like:

RPR: The problem space is that is the folks want to write type annotations on their JavaScript and not have a build step. WH: They can do this today by having types in comments. RPR: It's not ergonomic, and we see that in the wild that people only a few people are tipped over into using the full comment syntax. It's the Simplicity the developer experience. And yeah, the first class syntax is highly desired.

are simply misleading since the complement method is virtually unknown, hence the statistics used are biased. We have to ask people that have been exposed to both the super set and the complement method enough to not have misconceptions about them. I am one of them. The faster people understand that super set are inferior to complements, the better for the ecosystem.

david0178418 commented 2 years ago

@lillallol

The response is by no way thorough. In fact, for some strange reason, it completely omits mentioning JSDoc with TypeScript features, which I have mentioned before[0]. ...

Arguments used by the champions, like:

RPR: The problem space is that is the folks want to write type annotations on their JavaScript and not have a build step. WH: They can do this today by having types in comments. RPR: It's not ergonomic, and we see that in the wild that people only a few people are tipped over into using the full comment syntax. It's the Simplicity the developer experience. And yeah, the first class syntax is highly desired.

are simply misleading since the complement method is virtually unknown, hence the statistics used are biased. We have to ask people that have been exposed to both the super set and the complement method enough to not have misconceptions about them. I am one of them.

I am also one of them. This is the "gateway drug" that is often used to introduce types to straight js projects. After the value of types prove themselves, projects can then drift to full ts over time, one file at a time.

This method, while obviously better than not having types at all, is more cumbersome (as has been mentioned).

A concrete example. Consider the following component:

function Foo() {
    {/* Stuff */}

    return (
        <div>
            {/* More Stuff */}
        </div>
    );
}

Now assume I want to introduce some props. I can do the following with JSDoc:

/**
 * It's been a while, so there might be something slightly off
 * but the gist is at least roughly this.
 * @typedef {Object} Props
 * @property {String} foo
 * @property {Number} bar
 * @property {Boolean} baz
 * @param {Props} props
*/
function Foo(props) {
    {/* Stuff */}

    return (
        <div>
            {/* More Stuff */}
        </div>
    );
}

Or, I can do the following with an import

// types.ts
export interface FooProps {
    foo: string;
    bar: number;
    baz: boolean;
}

//index.js
/**@type {import("some/path/types").FooProps} */
function Foo(props) {
    {/* Stuff */}

    return (
        <div>
            {/* More Stuff */}
        </div>
    );
}

Compared these with

export interface Props {
    foo: string;
    bar: number;
    baz: boolean;
}

function Foo(props: Props) {
    {/* Stuff */}

    return (
        <div>
            {/* More Stuff */}
        </div>
    );
}

The first example is obviously much more verbose.

The second example is slightly less verbose, but still more so than typescript. But the main issue is the fact that a local concern has to be arbitrarily separated and is creating clutter in the common space.

However, in the third example, the local concern is able to remain local and there is minimal ceremony. Additionally, a tiered approach to exposing the types can be taken. In the event some consumers need to know about the props, the interface can be exported. In the event non-consumers need to be aware of the interface, then it can be graduated into the common space.

It is not EcmaScript that has to change for the bad design choice of TypeScript and Flow to introduce static type checking as super sets,

You have to provide more detail here. When invoking "bad design choice", specific examples of trouble should be provided.

On its face, this seems like a "separation of technologies" argument that is getting conflated with "separation of concerns". React is the prime example of how "bad practice" can and should be challenged. All the fears around entangling the view with logic were unfounded. Attempting to push all types into the commons is reminiscent of the unwieldy "views" folders which contained all the mustache or html files. Even during this time, many were already co-locating their templates with their code by coupling them into the same directory. Concerns that are inherently 1:1 have a gravitational pull that bring them as close as the technology conveniently allows.

In the end, this is objectively more ergonomic in terms of both visual clutter and mental overhead.

Does that make this proposal a given? Absolutely not. There is still plenty of discussion to be had. Are the costs worth the benefits? Would this hinder the language down the line? Would this create problems in the language today?

But the assertion that co-locating types with code is "bad practice" requires more concrete examples.

lillallol commented 2 years ago

After the value of types prove themselves, projects can then drift to full ts over time, one file at a time.

There is no reason for this to be done. It unnecessarily increases complexity by requiring you to compile and promotes bad practices like mixing intend with implementation.

This method, while obviously better than not having types at all, is more cumbersome (as has been mentioned).

You will get used to it. I felt the same way when I first got introduced to TypeScript, but then got used to it. Then when I tried the complement method I felt again the same way, but again got used to it. Also this:

/**@type {import("some/path/types").FooProps} */

is a matter of support to be converted to something more elegant like this:

//: "some/path/types".FooProps

So in the end people who are not used to this way, will indeed find it cumbersome, but eventually get used to it.

The second example is slightly less verbose

There are cases where it is actually less verbose even when compared to the super set method, especially when people want to reap the benefits of separation of intend and implementation.

But the main issue is the fact that a local concern has to be arbitrarily separated and is creating clutter in the common space.

From my experience, this is not an issue. Whenever I define a concretion in a .js file I define also its type in the same .js file. I then copy the name of the type and paste it in the JSDoc type tag. Then I move the type to the .ts file and trigger auto import in the JSDoc tag that already has the name of the type. If I ever want to find a specific type, then I have to remember which concretion is using it. I go to the file of the concretion and use the go to definition feature of the IDE for the imported type. I have also found my self in a sense segregating the common space of types by using the following files:

I have not faced anything that can be described as cluttering.

However, in the third example, the local concern is able to remain local and there is minimal ceremony. Additionally, a tiered approach to exposing the types can be taken. In the event some consumers need to know about the props, the interface can be exported. In the event non-consumers need to be aware of the interface, then it can be graduated into the common space.

Yeah I do the same thing. The whole type for Foo goes to the privateApi.ts. Props does not exist on its own yet. When there is a need for Props to exists because it is used in more than one places, or because I decided that it belongs to the public api, then Props is created and placed in types.ts or in index.d.ts respectively, and is imported from there from non-consumers and consumers respectively.

You have to provide more detail here. When invoking "bad design choice", specific examples of trouble should be provided.

this seems like a "separation of technologies" argument that is getting conflated with "separation of concerns"

Although this does indeed separate concerns, that is not the driving force. It is the empirically derived simplicity that is the driving factor, along with the benefits of separation of intend and implementation. There is no need to compile .ts to .js, not even to compile to .d.ts. There is no need for source maps. There is no need to depend on other packages for your code to execute. You do not have to worry how different configs work together. you do not have to worry about compile speeds as the project grows. TypeScript syntax will never collide with JavaScript syntax. There are many more like generics being valid JavaScript. I am in fact creating a proposal since months ago, that I have not yet published, and in fact proposes a type system agnostic (native) comment contract (similar to the one I have already mentioned) that If accepted will eventually force the super sets to be reduced to complements. Imagine an ecosystem where tsc as a compiler is discontinued. The ecosystem would be much healthier then. I just have to finish the (non trivial) projects that use the strict complement method, so that they act like a proof that the complement method is battle tested.

I am not blindly following axioms like "separation of concerns". The very fact that I use the complement method shows that I do not blindly follow. It is my practical experience that the complement method is superior in every way when compared to the superset method or even the (proposed) native superset method.

Regarding the super set method becoming native as is suggested by this proposal, we are irreversibly reserving syntax space, promote bad practices (mixing of implementation and intend) and force every JavaScript parser to change, to get something that we already have (TypeScript complement method). In the tc39 meeting some people have even mentioned that the proposed syntax can intrinsically not work.

I do not like the way the champions of this proposal and the author are handling the whole situation. They act like, the complement method does not exist. The only counter argument I have gotten is that you will not be able to use type assertions, while in fact the inability to use type assertions is an advantage since it produces safer static typing. I do not like the arguments they provide in the tc39 meeting, which are nothing more than biased statistics. Reading the tc39 meeting I just got the impression, that no matter what, they had to get stage 1.

You might ask what are the benefits of separation of intend and implementation. I can copy paste parts of my proposal if you want, that explain that thoroughly both from a designer but also a consumer perspective. By the way separation of intend and implementation naturally leads to the complement method.

codedread commented 2 years ago

I don't understand the terms "complement method" and "super sets" here. Are these alternative proposals ? Can someone link me to them?

orta commented 2 years ago

Languages like TypeScript and Flow are considered super-sets of JavaScript because they support all of JavaScript and then add extra features like types on top of it. This proposal is a super-set style proposal, which allows for carving out a space for these different languages to have their annotations in.

The use of the term 'complement' here generally refers to the idea that you can separate out the types entirely and reference them from existing JavaScript code, meaning you dont need to make changes to JavaScript to support type systems.

ljharb commented 2 years ago

@orta note that TS isn't actually a superset, though, in any way :-)

idleman commented 2 years ago

Type are not derived what the developer tell the system though, but what kind of operators that a specific value (efficiently) supports. Having a plus function for example:

function plus(a, b) {
  return a + b;
}

Tells every compiler a and b must support the "+" operator. So the best would probably be to 100% ignore TypeScript and its noisy type annotations and some way to tell the compiler this function require some parameter to support the "+" operator.

@require(a, operator('+'))
@require(a, operator('+'))
function plus(a, b) {
  return a + b;
}

The above method uses the same syntax as decorators and have some caveats, It would be better because we allow duck typing which is a common way in ECMAScript/JavaScript and still allow ensure static analysis.

The main trouble with type annotations in TypeScript is that those types cannot be trusted. All types in that language are actually just "maybe" types all together. You may ask for a int32, but you may get a picture of Santa Claus back instead (No offence!). We don´t want that in ECMAScript.

GrantGryczan commented 1 year ago

I think one huge use case you're missing here is Node.js projects, not just code that's shipped to the browser. There's often no benefit in minifying Node.js code, so avoiding a compilation step for type annotations would often allow avoiding compilation altogether.