proposal-optional-types

FAQ

Table of contents

Terminology

Where is this at?

This is, by far, what you’d call “drafty”, “strawman” or Stage 0 :)

We still have more questions than answers, but we think the direction is right.

We have a general intuition that this is a highly desirable feature, by the levels of adoption and maturity of existing type systems (some in place for over 10 years).

We also think we have collected solid data points (in the industry with interpreters and transpilers and in academia) to encourage us to look at Optional Type Systems (as opposed to sound gradual types).

We find more commonalities than disparities in existing production-ready type systems for JavaScript (typescript, flow and closure), but we acknowledge that there are disparities and that the devil is in the details.

We find the trend of adding types to dynamic languages (e.g. Python, and Dart) encouraging.

We don’t feel strongly about the specifics of the type system, in as much as we feel that one should exist :) To get the ball rolling, we start with a strawman proposal and hope we all collectively take it from here.

We don’t think this is not a novel idea. We believe that the circumstances are different and more favorable to revisiting the subject, more specifically:

Alternatives considered

Status quo?

What’s wrong with leaving things as is?

The biggest challenge we face keeping the status quo is twofold:

First, a javascript-based type system increases the chances we’ll see it adopted by in-browser developer tools (which already process javascript heavily), significantly decreasing the friction to access/use them. In addition to access, an in-browser type checker can increase the extent to which runtime errors are checked at debugging-time with in-browser debuggers (as an example of what can be made possible, see dart’s runtime checked mode).

Secondly, libraries written in one of the existing type system transpilers are not interoperable with the others: large teams needing to depend on or export libraries end up in an incohesive and incoherent compilation environment.

With the extensible web manifesto process in mind, our intuition is that we currently have a good balance between maturity/convergence/adoption and usefulness between the main polyfills/transpilers/compilers that is worth baking into the language the more popular/converged features.

Sound gradual typing?

An alternative to (unsound) optional typing is (sound) gradual typing, as it exists, for instance, in Typed Racket. When the type checker cannot prove that a type is correct, runtime checks are inserted. There are three main drawbacks with this approach:

Overall, sound gradual typing has so far only existed in a research setting, and it is not clear whether it can be made practical. The only type systems for dynamic languages that have succeeded in industry are optional type systems.

Decorators?

NOTE(domenic): in particular with additional decorator positions (such as functions and arguments) you could create most of a type system with decorators, with either runtime or AOT checks. It’s a bit of a stretch to put them on variable declarations… but still.

TODO(goto): come up with a few examples and explore them a bit more.

Macros?

One of the most promising/interesting approach to be explored is introducing macros (hint, not the #define-like macros, but lisp-like macros) to JavaScript: the parsing time ability to meta-program (code that changes code).

Macros are great for defining little languages within a language that are suited for specific problems (see the sweetjs project). However, it is not clear what the advantages are of doing a type system using macros:

Considerations

Does this need to be part of the language?

Our intuition is that it does, for the same reasons that keeping the status quo is not ideal:

We also think that tc39 is the most natural venue to standardize a typechecker.

Does this grow the language unnecessarily?

Guy Steele does a really good job at describing the challenges of growing a language. Mark Miller does as well in his excellent article The Tragedy of the Common Lisp, or, Why Large Languages Explode.

The fundamental challenge here is that introducing new syntax comes with a cost and the cost gets compounded with time: users learning the language need to be introduced to new (and complicated!) concepts and existing users eventually get in contact with the features while reading/embedding external libraries.

There is a desire to keep the language small enough that one can understand it completely.

As Mark also acknowledges in the article, the introduction of classes in ES6 came with the cost of growing the language but was justified by its massive benefits.

Even though types increase complexity by increasing the size of the language, they reduce complexity in other ways. First, programmers often code with types in mind, even in dynamic languages. Having a type system allows programmers to document their invariants, and have the machine check these invariants. Also, when someone wants to use a third-party library, having type definitions for the library makes it easier to understand how to call library methods. Last, JavaScript operators work on values of most types, and the conversion rules are very complex and hard to remember (for a fun take on this, see this talk). A type system removes that complexity by simply forbidding most implicit conversions, e.g., the minus operator can only take numbers.

By proposing an optional type system instead of a sound gradual one, it becomes possible for developers who prefer untyped JS to largely ignore types. They can write in untyped JS and call into typed code without any changes to their code base.

For these reasons, we find that the cost/benefit ratio is well balanced.

What’s the relationship between this and typed arrays?

TODO(goto): articulate this better. NOTE(adamk): Not sure what this question means…are you worried people will confuse this use of “type” for the one in “typed arrays”? I’d be surprised by that (but would be interested if you’ve heard that feedback).

Are transpilers still needed?

Yes.

Along the lines of the Extensible Web Manifesto, it is critical to the long-term health of the web to tighten the feedback loop between editors of web standadrs and web developers. Transpilers have played and will continue to play two important roles:

For example, in many ways transpilers pioneered things like (a) classes, (b) public/private fields and (c) modules before they were baked into the language, showing the way. Along the same lines, their ability to innovate more quickly on type system features like (a) generics, (b) operator overloading, (c) enums, etc is a key part of the process to bake them into the language’s type system.

We hope that transpilers will continue to play a role prototyping, experimenting and innovating, fundamentally polyfilling features for javascript that simplify and streamline the process of standardizing APIs that already have implementations and significant real-world usage.

Is TC39 the right venue?

We believe that the type system should be baked into the standard language and that TC39 is the right venue. However, this proposal is primarily targeted towards a typechecker used at development-time rather than production-time interpreters, and, in its current formation, there aren’t that many recurring representatives of developer tools (e.g. compilers, transpilers, minifiers, debuggers, profilers, etc).

Our hope is that TC39 would be open to introducing a venue/channel where authors of tools feel comfortable and interested to come and collaborate on the evolution of the typesystem.

@erights: from a specification perspective, to solidify the separation between the interpretation semantics and the typechecking semantics, it might be worth exploring describing them in separate docs, such as how JSON and i18n is handled.

Network and parsing cost?

Adding extra syntax to the language that is meant to be ignored at runtime can lead to developers sending extra bytes on the wire that will be parsed but not used.

To mitigate that, one strategy would be to rely on minifiers/preprocessors/compilers to strip the metadata out prior to deployment. We think that a combination of (a) not using the optional type syntax, (b) paying the neglectable runtime cost for small apps that are already paying the cost with comments or (c) paying the transpiler/compiler/minifier cost offers a reasonable spectrum of solutions for developers.

Backwards compatibility?

If a developer uses the type system, how do they deal with browsers that don’t support it yet (and will throw syntax errors)? Does that force one to use minifiers (i.e. as opposed to other JavaScript features where one can check for their existence on the fly)?

This seems like an analogous challenge with any proposal that changes the syntax of the language (e.g. async/await), so most/all of the trade-offs apply.

Prior Art

This is by far not a new idea.

It appears in other programming languages, as polyfills and as previous proposals to JavaScript. Here are the most relevant things we have looked at (feel free to let us know if we are forgetting anything in particular).

Other languages?

Python and Dart are most probably the closest analogies so we start with those. We go over a complete set of languages we found had related/interesting type systems at the end of this section.

Python

From PEP 0484 and PEP 482.

“While these annotations are available at runtime through the usual annotations attribute, no type checking happens at runtime . Instead, the proposal assumes the existence of a separate off-line type checker which users can run over their source code voluntarily.”

“The type system supports unions, generic types, and a special type named Any which is consistent with (i.e. assignable to and from) all types.”

TODO(goto): give a better overview of PEP0482. TODO(goto): chat with @collinwinter to gather ideas from python.

>>> def greeting(name: str) -> str:
    return 'Hello ' + name
>>> greeting('Sam')
'Hello Sam'
>>> greeting.__annotations__
{'name': <class 'str'>, 'return': <class 'str'>}

No first-class syntax support for explicitly marking variables as being of a specific type is added by this PEP. To help with type inference in complex cases, a comment of the following format may be used:

x = []  # type: List[Employee]
x, y, z = [], [], []  # type: List[int], List[int], List[str]
x, y, z = [], [], []  # type: (List[int], List[int], List[str])
a, b, *c = range(5)   # type: float, float, List[float]
x = [
   1,
   2,
]  # type: List[int]

By default generic types are considered invariant in all type variables, which means that values for variables annotated with types like List[Employee] must exactly match the type annotation – no subclasses or superclasses of the type parameter (in this example Employee ) are allowed.

To facilitate the declaration of container types where covariant or contravariant type checking is acceptable, type variables accept keyword arguments covariant=True or contravariant=True .

Dart

TODO(goto): write down about dart.

TC39 discussions?

In TC39 this isn’t a new idea either. Here are the discussions we were able to find chronologically (feel free to send us links if you have any that we are missing here):

The Type System

Strategy

Sequencing?

Here are some of the features available in TypeScript/Flow/Closure that we chose to leave as future work. We don’t believe leaving any of these features out will corner ourselves into adding them later.

New ideas?

Our general strategy is to pick a conservative subset of existing typesystems, rather than invent new things. We believe that transpilers are a much more effective venue to innovate and experiment and that, with the test of time, features move from them to the standard language. We encourage you to work with the transpilers and use them as a place to prototype your ideas and gather users before extending the MVP.

Open Design Questions?

This is more restrictive than JS. Is that on purpose?

For example one may feel very comfortable doing -x when one knows x is either a number or a string which I want to convert to a number; The alternative proposed here would require something like -Number(x).

That’s an accurate observation and that’s an accurate representation of the intention.

The three type systems made the choice to exclude some valid programs in the interest of finding useful warnings.

If we wanted to only warn for things that throw, then we would warn when calling a non-function, and when accessing properties on null/undefined, but not much else. Most JS operators work on all types, even though they are used with only a subset of types the vast majority of the time.

NOTE(domenic): there is a conflict between static analysis and dynamic analysis. Consider the example let x : number = true ? 0 : false; A runtime type system would have no problem with this, whereas what you’re proposing would disallow it. This is probably worth highlighting at a higher level in the document: it seems like you are leaning toward a statically-checkable model, which there is precedent for for JS but I believe other optionally-typed languages went a different direction.

Are you breaking the web?

Nope. See are all existing programs correct?.

Are all existing programs correct?

NOTE(erights): specifically, are they all still statically valid? and if not, how do you avoid #breakingtheweb?

Interpreters remain unchanged, hence keeping all existing programs correct.

For the newly introduced typechecker some (previously valid) programs may be (desirably) invalidated.

var x = 'foo'; // OK
var y= x - 1; // OK
var z = 1 - true; // Error
// Typechecker Error, although Interpretation remains semantically and statically valid.

What’s the default for untyped programs?

All unannotated variable/parameter/etc is the same as annotating it with Any.

NOTE(erights): does all untyped code default to Any? We found in our experience with another language that it is useful sometime to use an unamed Type to be a better default in certain occasions / compilation contexts.

Shorthands?

TODO(goto): for nullable types, optional types and arrays.

Are varargs supported?

TODO(goto, domenic): are they?

Are spread operators supported?

TODO(goto, domenic): are they?

Generics

Can you get away without generics?

We all agree that we want generics long term.

The choice to omit them now is just because of complexity. Specifying the type semantics will be very difficult, and we want to avoid as much difficulty as we can in the first version. We believe that the most significant hurdle for types in JS is to agree as a community on the kind of type system we want, and if we manage to get a type system in the standard, then progress on subsequent features will be easier.

From the experience with implementing type systems, we believe that generics are one of the most complex parts of the system, especially when combined with union types (so, Java generics are easier for instance).

Another very complex part to specify is flow sensitive typing. All three type systems have it, and we want it in the standard, but we chose to leave it out of the first version.

If we leave out generics from v0 and add them in v1, backwards compatibility is possible because generics can always be instantiated with the any type, so Promise would be the same as Promise.

Then again, as we say in other places, most aspects of this proposal are subject to change. So, I wouldn’t be surprised if enough people want generics in v0 and we end up including them.

Are Arrays, Promises and Iterables useful without generics?

And what about Thenable<>? Generator<>? Map<>/Set<>/WeakMap<>/WeakSet<>?

Are you sure that with that strategy to special-case Arrays you can make it 100% backwards compatible?

NOTE(domenic): I’m familiar with at least C++ and C# as two languages that special-cased arrays before introducing generics and the result was a terrible mismatch carried forward into eternity. It’s a pretty different situation, but it raises a warning alarm for me :).

NOTE(domenic): Basically C# arrays never implemented the generic collection hierarchy, so were incompatible with code that assumed generics. This led to advice that library authors write overloads, one for non-generic arrays and one for generic collections. And when library authors did not, authors needed to go through casts. Authors also needed to go through casts whenever they wanted to use language features that assumed a generic collection (e.g. LINQ). As I said, a pretty different situation than what’s being done here :). Just a general area of warning bells.

What are the types of access to arrays?

For example, in let a: Array[string];, does one need to define let b : string undefined = a[0]; ? does that mean that the result of accessing an array can always lead to undefined? https://flow.org/en/docs/types/arrays/

NOTE(dimvar): This is a classic example of why unsound type systems are necessary for javascript, because bounds in array are always unchecked. NOTE(dimvar): You can either make it sound by making the runtime check, or you can add the undefined everywhere (which is non ergonomical) and otherwise dependent types.

What about if I do = undefined?

Trailing parameters with default values should be considered optional by the type checker.

NOTE(domenic): This seems subtle and hard to get my head around. (But not a real problem, just something tricky to work through.) E.g. if I declare this function: function a(value: number = 5) is that a “syntax error” since I didn’t write number undefined?