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

Type files #145

Open prettydiff opened 2 years ago

prettydiff commented 2 years ago

Problem

It feels like there are two goals to this proposal:

  1. Do not violate backwards compatibility, especially in consideration of syntax
  2. Do not require a separate compile step for type checking

I have reservations about using comments, because comments are meant to be thrown away. This has all kinds of problems from preprocessors for documentation schemes to minifiers and source maps and and so forth.

Counter Suggestion

type files

Instead my recommendation is to propose type files, similar to TypeScript's .d.ts files. These type files can be imported using ES6 module conventions and used against identity expressed in the code. Because the type declarations occur in a separate file no changes of any kind are immediately required of the JavaScript code file, aside from one or more import statements that provides a type:

import type from "./my_type_declarations.tjs"

In this code example I am using a fictitious file extension tjs to contain typed JavaScript, but the file extension can be anything new or prior existing. There is one piece of syntax change. Notice the type keyword. That keyword identifies the incoming code is type declarations opposed to regular imported JavaScript code.

The primary security benefit of this recommendation is that type declarations are applied and expressly limited to typed identities in the current code file that import types, but not in other code files. This prevents the application of types against code that does not wish to be typed, having no type imports, and also allows for separation of concerns in that different code files can import different type files.

Identity

In order for this to apply to code without further syntax changes that allow for a 1-1o-1 mapping of type identity, as applied in TypeScript with the variable : type mapping model, it would apply to code identity. Code identity refers to the names code authors apply to code such that these names are syntax tokens not defined as language keywords, such as: function names, variables, classes, and prototype names.

The natural precedence imposed by the language specification for name resolution is function names first, variable names second, and class/prototype names third. This precedence occurs as a means to resolve reference location in a lexical model allowing for usage of implied global variables. Implied global variables, variables that are not formally declared and thus are implicitly declared in global scope, are no longer allowed in the strict model proposed by Crockford, adopted by ES5 as "use strict";, and silently imposed by use of ES6 modules. This reference model continues to exist in the language even though implicit global variables are now generally shunned.

In this precedence function identity achieves highest priority because functions provide scope. Before ES6 the language did not have block scope, so functions were the only scope convention. Also, blocks have no identity even with block scope, such that an arbitrary block cannot be named or assigned to a variable. Functions are also internally aware of their names, which provides access to a containing function without lexical name resolution, which is critical to a variety of internal mechanisms in the language.

Class and prototypes are lowest priority of identity resolution because of the lexical model. Classes are a syntax mapping to prototypes and prototype resolution occurs only after lexical resolution of a name through the scope chain is exhausted.

This prior existing identity model can be used to create a type resolution/declaration model.

Type Syntax

Since type declarations are completely new to JavaScript and, as suggested here, externalized from the JavaScript code the syntax can be of any form without conflict to the already existing language syntax.

matthew-dean commented 2 years ago

I think the challenge of that from a developer perspective is that it's easy to quickly have drift between a type-definition file and the actual runtime code. That is, how would you smoothly manage the integration of the two?

ljharb commented 2 years ago

Also, the language doesn’t have the concept of “files” - so this would have to be a third parse goal, and we’d have to just hope that every implementation did the sensible thing and implemented them as separate files, only with a distinct mime type/extension. But, since that’s not what happened with the Module parse goal, i don’t think there’s a viable path here.

lillallol commented 2 years ago

@matthew-dean

That is, how would you smoothly manage the integration of the two?

That is the job of the static type checker.

@ljharb

Also, the language doesn’t have the concept of “files”

Then how does it has modules?

a third parse goal

What is that? You mean that it would be the job of external to JavaScript tooling?

we’d have to just hope that every implementation did the sensible thing and implemented them as separate files

There will always be people who will abuse proposals.

only with a distinct mime type/extension.

Can't tc39 standardize the file extension? Or at least the import path have extension-less file name?

the Module parse goal

what is that?

ljharb commented 2 years ago

@lillallol in the language, Modules exist, but a Module is not a file. The language has no such concept.

TC39 can not and would not standardize a file extension or a MIME type; there's other standards venues for that that are beyond our scope.

The Module parse goal is contrasted to the Script parse goal - it's the difference between a classic script (in sloppy mode, has with, etc) and a Module (has import/export statements, in strict mode, etc).

lillallol commented 2 years ago

The Module parse goal

So this : <script src="path/to/fie.js"></script> added type="module" and went to this <script type="module" src="path/to/fie.js"></script> to avoid adding a different than .js extension?

In the case of what OP is suggesting, since the static types are not needed when .js gets executed, then all we need to do is to standardize the path pattern of import type from "./my_type_declarations.tjs". I think whatever follows the last / should not have a ., so the path becomes type system agnostic. Let third party tools deal with defining the type system and applying static type checking (for now). But then someone can ask:

if we are letting third party tools to deal with static type checking then why even bother to standardize import type from "./my_type_declarations.tjs"

because separating intend and implementation (among the many benefits it has) is actually a pragmatic, minimal risk, minimal work, first step for introducing static typing in EcmaScript. A tc39 standard on how types are supposed to be imported and applied, can be type system agnostic, so all type systems can adhere to. This is a first step to put pressure to the ecosystem and will inevitably lead the super sets to become complements.

The scope of tc39 according to ecma-international is:

Scope:

Standardization of the general purpose, cross platform, vendor-neutral programming language ECMAScript®. This includes the language syntax, semantics, and libraries and complementary technologies that support the language.

so as far as I understand static typing belongs to the scope of tc39. In my opinion sooner or later tc39 will have to do something about static type checking. The ecosystem, since some people decided to mix implementation and intend, is moving in the wrong directions and opened the pandora's box. Now people are trying to make that, native to the language, as it can be seen from the proposal of the context repository. This has to stop. tc39 has to do something.

there's other standards venues for that that are beyond our scope.

You mean that belongs to the scope of a different technical group to standardize?

prettydiff commented 2 years ago

File Extensions

File extensions are irrelevant to a JavaScript interpreter. If you point a JavaScript interpreter at any file it will attempt to parse the contents of that file as JavaScript irrespective of any file extension. If type files become a thing I suspect in that case file extensions would be irrelevant there too. All that matters is that an interpreter is given contents of a file and told it parse it as type declarations.

Different execution contexts do, however, impose assumptions about files before feeding files to a respective interpreter. For example Node.js needs a file name to execute but if no file extension is supplied it will execute any filename of extension .js if the remainder of the file name otherwise matches the supplied second shell argument.

Example: node startup will execute a file named startup.js if locally available in the current working directory. This is not a feature of the language, but an assumption built into Node. Likewise the browser appears to ignore file extensions entirely when executing script files and instead relies upon media types to determine the appropriate interpreter: <script type="text/javascript" src="myFile.php"></script>. In the absence of a type definition the browser will default to something like text/javascript or application/javascript.

File Scope

@ljharb is correct in that the language has no concept of files. The module system is a weird state of affairs in this regard. The module system is defined by the language specification and requires interpretation of language instructions in a given file like all other JavaScript. However, the module system appears to execute in a context outside of other language execution, a separate execution context to resolve artifacts outside the language, such as the file system, into language reference points (functions and variables). This separate execution context is reinforced by these qualities:

Most relevant to this discussion is the third bullet point. It means a reference declared in an import statement is available to the entirety of the file containing the declaration. This is not a globally scoped variable for applications containing a plurality of code files. As an example of global variables in an application of numerous files consider Node's process and global variables. Since the language has no concept of files there is not a file division of code at execution time, so therefore changes to a globally scoped variable are available to the entirety of the application.

This is what makes import references weird when thinking about scope. Import references are not declaration points. They are maps to other references declared elsewhere, like creating a new variable whose value is a different variable. The rules of passed by reference continue to apply. Although the values are not new, the reference names are. The availability, I guess this is scope, of those new reference names is the entirety of the respective file. This is weird because the language has no appreciation of files. Its just a bunch of interpreted code in memory. This supposed file scope is not a feature of the language runtime, but a feature of the module system in its external context and whatever reference mapping scheme the module system uses.

Type Context

The module system is a context completely externalized from language execution. A type system would also be a separate execution context. The language would not have to achieve an awareness of files. Instead a type system would have to achieve a reference map, as the module system already does, as necessary to associate a type definition to a given reference declaration. Like the module system, this must occur at initial code interpretation and not at instruction execution time.

Drift

@matthew-dean mentioned concern for drift. I do not anticipate any increased risk here. If type declarations are in a separate file and that type file is imported into the target code file the import statement defines the relationship between the two files. Any changes to the target code file that violate the definitions in the type file would raise type violations as is the case with TypeScript. If a developer chooses to not keep their type definitions updated two things can happen:

  1. Variables in the target code file will not be type checked if no such types are declared in the type file.
  2. Types declared in the type file may become stale such that the corresponding target code no longer exists and these types are unused.

Both of those conditions are problems that currently exist in TypeScript. To my knowledge, and I am likely wrong here, there is not an audit convention supplied with the TypeScript compiler to warn on unused type definitions and there is not an indicator to warn on untyped variables.

dfabulich commented 2 years ago

IMO, this doesn't address the problem that this proposal is intended to solve.

At the TC39 meeting in March, @DanielRosenwasser (TypeScript Program Manager at Microsoft) presented this problem statement:

Problem Statement The strong demand for ergonomic type annotation syntax has led to forks of JavaScript with custom syntax. This has introduced developer friction and means widely-used JS forks have trouble coordinating with TC39 and must risk syntax conflicts.

Proposal Formalize an ergonomic syntax space for comments to integrate the needs of type-checked forks of ECMAScript.

The problem statement isn't "There's no way to write type-safe JS without transpiling," because there clearly is: for example, JSDoc allows you to write type-safe JS without transpiling. (Flow's "comment types" syntax is even better IMO. https://flow.org/en/docs/types/comments/ )

But JSDoc isn't "good enough," and specifically it isn't "ergonomic enough." The proof of that is simply that developers decided to fork JS (and use forks of JS) rather than use JSDoc.

Would import type from "./my_type_declarations.tjs" solve the problem? Not really.

The thing is, if all we're adding to the syntax is import type from "./my_type_declarations.tjs", it's barely any additional effort to use comment syntax for it. Echoing Flow's syntax:

//:: import type from "./my_type_declarations.d.ts"

We could all do that now. But we don't; we do types in TypeScript and transpile them away.

I claim that's because declaring types in a separate file isn't ergonomic enough; it doesn't "feel nice enough." In particular, it requires you to re-declare every object, every function, every parameter, etc. in the separate file. And then you have to remember to jump over and fix the other file whenever you change something.

You could cook up a linter today that would automagically type-check .d.ts types with //:: syntax, and then try to convince every TypeScript developer to stop using TypeScript and instead exclusively use .d.ts files, but you would not succeed. TS developers would continue using TS.

In other words, anyone suggesting an alternative proposal to what was presented to TC39 has a very difficult target to hit: the syntax would have to be so great that it would convince the TypeScript team to adopt it, and thereby persuade the masses of TS developers to switch to the new thing and heal the fork.

This is why this current proposal is so important, relative to https://github.com/samuelgoto/proposal-pluggable-types which is cited from the README. That "pluggable types" proposal is essentially the same as what was presented to TC39, but, at the time, there were no signals from the TypeScript team that they would disarm and adopt the new thing if it were accepted.

The TS team is on board with the TC39 proposal; in principle, it could heal the fork. import type from "./my_type_declarations.tjs" could not do that (not without the rest of the TC39 proposal), and so it won't address the problem statement.

prettydiff commented 2 years ago

So this idea provides no preference for syntax by any means. Type checking is something that has never before existed in the language so the syntax can be what ever people want.

My primary concerns with the proposal in its current form are:

For any type system provided I can settle for a compile step. A compile step is a pain, but the benefits of the type definitions outweigh the costs of the compile step, so its a frustration I am willing to tolerate, but the absence of a compile step would be ideal.

To be extremely clear an import statement, at least in this manner, solves only 1 problem: defines a relationship between documents. If the syntax around that important statement should be different than so be it. Again, I am not proposing any syntax.

We could all do that now. But we don't; we do types in TypeScript and transpile them away.

Yes, but the context is different. TypeScript is a separate language, and so to make that work there are only two options:

  1. Compile the TypeScript to JavaScript
  2. Interpret and execute the TypeScript directly (this doesn't happen)

I claim that's because declaring types in a separate file isn't ergonomic enough; it doesn't "feel nice enough." In particular, it requires you to re-declare every object, every function, every parameter, etc. in the separate file. And then you have to remember to jump over and fix the other file whenever you change something.

I am not disagreeing with that, but this problem already exists in TypeScript. If I need to refactor something I have to change the code and the corresponding type definition to match. The only way to avoid this problem that I can see is to either stop using types or invent a new language where all types are completely inline and the language executes as written. That exceeds the scope of the proposal more than anything I can imagine.

The TS team is on board with the TC39 proposal; in principle, it could heal the fork. import type from "./my_type_declarations.tjs" could not do that (not without the rest of the TC39 proposal), and so it won't address the problem statement.

The matter of agreement and conformance are very real concerns I have not even attempted to address. In all fairness to the proposal this is not a matter of consideration for TypeScript. It is a matter of consideration for enhancing the ECMAScript specification before TC39. I am not trying to be argumentative, but this proposal can be completely divergent from anything complementary to TypeScript without harm.

lillallol commented 2 years ago

@dfabulich

IMO, this doesn't address the problem that this proposal is intended to solve.

At the TC39 meeting in March, @DanielRosenwasser (TypeScript Program Manager at Microsoft) presented this problem statement:

Problem Statement The strong demand for ergonomic type annotation syntax has led to forks of JavaScript with custom syntax. This has introduced developer friction and means widely-used JS forks have trouble coordinating with TC39 and must risk syntax conflicts.

Proposal Formalize an ergonomic syntax space for comments to integrate the needs of type-checked forks of ECMAScript.

The problem statement isn't "There's no way to write type-safe JS without transpiling," because there clearly is: for example, JSDoc allows you to write type-safe JS without transpiling. (Flow's "comment types" syntax is even better IMO. https://flow.org/en/docs/types/comments/ )

First of all provide a citation for your claim. Secondly, you are actually calling the creators of this proposal, liars.

But JSDoc isn't "good enough," and specifically it isn't "ergonomic enough." The proof of that is simply that developers decided to fork JS (and use forks of JS) rather than use JSDoc.

Please refrain from argumentum ad populum. Also JSDoc (without ts) is a tool for documentation generation, hence it makes no sense to compare to TypeScript.

Would import type from "./my_type_declarations.tjs" solve the problem? Not really.

The TS team is on board with the TC39 proposal; in principle, it could heal the fork. import type from "./my_type_declarations.tjs" could not do that (not without the rest of the TC39 proposal), and so it won't address the problem statement.

You people are arbitrarily shifting the goal posts each time you are faced with an argument you do not like. The aim of this proposal is what its README.md claims. What op is suggesting is actually better than the proposal (even if your claims about the intend of this proposal are true).

Regarding the super sets, the very fact that they are super sets and not complements, makes them a mistake. I can provide literally around 30 arguments supporting that. By no way we should strive for making them native to JavaScript.

The thing is, if all we're adding to the syntax is import type from "./my_type_declarations.tjs", it's barely any additional effort to use comment syntax for it. Echoing Flow's syntax:

//:: import type from "./my_type_declarations.d.ts"

We could all do that now. But we don't; we do types in TypeScript and transpile them away.

This is the hype right now. If each time something is in hype we make it native, then the web would have been broken many years ago.

I claim that's because declaring types in a separate file isn't ergonomic enough; it doesn't "feel nice enough." In particular, it requires you to re-declare every object, every function, every parameter, etc. in the separate file. And then you have to remember to jump over and fix the other file whenever you change something.

These are low effort excuses. For example strict mode of typescript makes you write more code, but it helps you produce maintainable code. You are actually promoting bad practices. Go read the classic : Dependency Injection. And no, you do not need to do dependency injection to reap the benefits of separation of intend and implementation

You could cook up a linter today that would automagically type-check .d.ts types with //:: syntax, and then try to convince every TypeScript developer to stop using TypeScript and instead exclusively use .d.ts files, but you would not succeed. TS developers would continue using TS.

Until a tc39 proposal that reserves syntax for something not related to static typing introduces breaking changes to TypeScript. The ecosystem then would be so much more healthy. Also there is no need to cook up any linter. What the op suggest is actually enabled by TypeScript already.

In other words, anyone suggesting an alternative proposal to what was presented to TC39 has a very difficult target to hit: the syntax would have to be so great that it would convince the TypeScript team to adopt it, and thereby persuade the masses of TS developers to switch to the new thing and heal the fork.

Well the syntax is already adopted, and I made my case about adoption before. Hype it and people will just follow.