• Stars
    star
    4,252
  • Rank 10,155 (Top 0.3 %)
  • Language
    JavaScript
  • Created almost 4 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

ECMAScript proposal for type syntax that is erased - Stage 1

⚠️ This document has not been updated since the March 2022 Plenary Discussion of TC39. Details will change in the coming days.

ECMAScript proposal: Type Annotations

This proposal aims to enable developers to add type annotations to their JavaScript code, allowing those annotations to be checked by a type checker that is external to JavaScript. At runtime, a JavaScript engine ignores them, treating the types as comments.

The aim of this proposal is to enable developers to run programs written in TypeScript, Flow, and other static typing supersets of JavaScript without any need for transpilation, if they stick within a certain reasonably large subset of the language.

A tentative grammar for this proposal is available here.

Status

Stage: 1

Authors:

  • Gil Tayar
  • Daniel Rosenwasser (Microsoft)
  • Romulo Cintra (Igalia)
  • Rob Palmer (Bloomberg)
  • ...and a number of contributors, see history.

Champions:

  • Daniel Rosenwasser (Microsoft)
  • Romulo Cintra (Igalia)
  • Rob Palmer (Bloomberg)

Motivation: Unfork JavaScript

Over the past decade, the case for static type-checking has been proven out fairly successfully. Microsoft, Google, and Facebook released TypeScript, Closure Compiler, and Flow, respectively. These efforts have been large investments in JavaScript to reap the productivity gains they saw in other statically-typed languages including finding errors earlier on, and leveraging powerful editor tooling.

In the case of TypeScript, Flow, and others, these variants of JavaScript brought convenient syntax for declaring and using types in JavaScript. This syntax mostly does not affect runtime semantics, and in practice, most of the work of converting these variants to plain JavaScript amounts to erasing types.

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 JavaScript forks have trouble coordinating with TC39 and must risk syntax conflicts. This proposal formalizes an ergonomic syntax space for comments, to integrate the needs of type-checked forks of JavaScript.

Community Usage and Demand

Static Typing was the most requested language feature in the State of JS survey in both 2020 and 2021.

missing-features-js

Additionally, TypeScript was currently listed as the 4th most-used language in GitHub's State of the Octoverse, and on Stack Overflow's Annual Developer Survey it has been listed in both the top 4 most-loved languages since 2017 and the 10 most-used languages.

Trends in JavaScript Compilation

The rise of type syntax in JavaScript coincided with the rise of downlevel compilation (sometimes called "transpilation"). As ES2015 was standardized, JavaScript developers saw wonderful new features they could not immediately use because of constraints around supporting older browsers. For example, an arrow function could provide developer ergonomics, but wouldn't run on every end-user's machine. As a result, projects like Traceur, TypeScript, and Babel filled the gap by rewriting ES2015 code into equivalent code that would work on older runtimes.

Because type syntax is not natively supported in JavaScript, some tool had to exist to remove those types before running any code. For type systems like TypeScript and Flow, it made sense to integrate a type erasure step with a syntax-downleveling step, so that users wouldn't need to run separate tools. More recently, some bundlers have even started doing both.

But over time, we anticipate there will be less need for developers to downlevel-compile. Evergreen browsers have become more of the norm, and on the back-end, Node.js and Deno use very recent versions of V8. Over time, for many TypeScript users, the only necessary step between writing code and running it will be to erase away type annotations.

Build steps add another layer of concerns to writing code. For example, ensuring freshness of the build output, optimizing the speed of the build, and managing sourcemaps for debugging, are all concerns that JavaScript initially side-stepped. This simplicity made JavaScript much more approachable.

This proposal will reduce the need to have a build step which can make some development set-ups much simpler. Users can simply run the code they wrote.

Limits of JSDoc Type Annotations

While build tools are not terribly difficult to use, they are yet another barrier to entry for many developers. This is in part why the TypeScript team invested in support for expressing types in JSDoc comments. JSDoc comments had some existing precedence in the JavaScript community for documenting types, and these types were leveraged by the Closure compiler.

This comment convention is often found in build scripts, small web apps, server-side apps, and elsewhere where the cost/benefit tradeoff of adding a build-tool is too high. Even when TypeScript doesn't provide type-checking diagnostics, the comment convention is still leveraged in editor functionality because TypeScript powers the underlying JavaScript editing experience.

Here's an example of the JSDoc-based type syntax from TypeScript's JSDoc Reference.

/**
 * @param {string}  p1 - A string param.
 * @param {string=} p2 - An optional param (Closure syntax)
 * @param {string} [p3] - Another optional param (JSDoc syntax).
 * @param {string} [p4="test"] - An optional param with a default value
 * @return {string} This is the result
 */
function stringsStringStrings(p1, p2, p3, p4="test") {
    // TODO
}

And here's the equivalent TypeScript syntax enabled by this proposal.

function stringsStringStrings(p1: string, p2?: string, p3?: string, p4 = "test"): string {
    // TODO
}

JSDoc comments are typically more verbose. On top of this, JSDoc comments only provide a subset of the feature set supported in TypeScript, in part because it's difficult to provide expressive syntax within JSDoc comments.

Nevertheless, the JSDoc-based syntax remains useful, and the need for some form of type annotations in JavaScript was significant enough for the TypeScript team to invest in it.

For these reasons, the goal of this proposal is to allow a very large subset of TypeScript syntax to appear as-is in JavaScript source files, interpreted as comments.

Proposal

The following is a strawperson proposal. Please treat it as such.

Type Annotations

Type annotations allow a developer to explicitly state what type a variable or expression is intended to be. Annotations follow the name or binding pattern of a declaration. They start with a : and are followed by the actual type.

let x: string;

x = "hello";

x = 100;

In the example above, x is annotated with the type string. Tools such as TypeScript can utilize that type, and might choose to error on the statement x = 100; however, a JavaScript engine that follows this proposal would execute every line here without error. This is because annotations do not change the semantics of a program, and are equivalent to comments.

Annotations can also be placed on parameters to specify the types that they accept, and following the end of a parameter list to specify the return type of a function.

function equals(x: number, y: number): boolean {
    return x === y;
}

Here we have specified number for each parameter type, and boolean for the return type of equals.

Type Declarations

Much of the time, developers need to create new names for types so that they can be easily referenced without repetition and so that they can be declared recursively.

One way to declare a type - specifically an object type - is with an interface.

interface Person {
    name: string;
    age: number;
}

Anything declared between the { and } of an interface is entirely ignored.

While interfaces can extend other types in TypeScript, rules here are to be discussed.

A type alias is another kind of declaration. It can declare a name for a broader set of types.

type CoolBool = boolean;

Classes as Type Declarations

This proposal would allow class members, like property declarations and private field declarations, to specify type annotations.

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    getGreeting(): string {
        return `Hello, my name is ${this.name}`;
    }
}

For most type-checkers, annotated class members would contribute to the type produced by constructing a given class. In the above example, a type-checker could assume a new type named Person, with a property name of type string and a method getGreeting that returns a string; but like any other syntax in this proposal, these annotations do not weigh into the runtime behavior of the program.

Kinds of Types

The above examples use type names like string, number, and boolean, but TypeScript and others support types with more involved syntax than just a single identifier. Some examples are given in the following table.

Name Example
Types References with Type Arguments Set<string>
Object Types { name: string, age: number }
Array Type Shorthand number[]
Callable Type Shorthand (x: string) => string
Constructable Type Shorthand new (b: Bread) => Duck
Tuple Types [number, number]
Union Types string | number
Intersection Types Named & Dog
this Types this
Indexed Access Types T[K]

This table is not comprehensive.

The goal of this proposal is to find a reasonable set of syntactic rules to accommodate these constructs (and more) without prohibiting existing type systems from innovating in this space.

The challenge with this is denoting the end of a type - this involves stating explicitly what tokens may and may not be part of a comment. An easy first step is to ensure that anything within matching parentheses and brackets ((...), [...], {...}, or <...>) can be immediately skipped. Going further is where things get harder.

These rules have not yet been established, but will be explored in more detail upon advancement of this proposal.

See also

Parameter Optionality

In JavaScript, parameters are technically "optional" - when arguments are omitted, the parameters of a function will be assigned the value undefined upon invocation. This can be a source of errors, and it is a useful signal whether or not a parameter is actually optional.

To specify that a parameter is optional, the name of that parameter can be followed with a ?.

function split(str: string, separator?: string) {
    // ...
}

Importing and Exporting Types

As projects get bigger, code is split into modules, and sometimes a developer will need to refer to a type declared in another file.

Types declarations can be exported by prefixing them with the export keyword.

export type SpecialBool = boolean;

export interface Person {
    name: string;
    age: number;
}

Types can also be exported using an export type statement.

export type { SomeLocalType };

export type { SomeType as AnotherType } from "some-module";

Correspondingly, another module can use an import type statement to reference these types.

import type { Person } from "schema";

let person: Person;

// or...

import type * as schema from "schema";

let person: schema.Person;

These type-specified declarations act as comments as well. They would not trigger any resolution or inclusion of modules. Similarly, their named bindings are not validated, and thus there is no runtime error if a named binding references an entity that was never declared. Instead, design-time tools would be free to statically analyze these declarations and issue an error under such conditions.

Type-Only Named Bindings

Maintaining separate import statements for values and types can be cumbersome - especially when many modules export both types and values.

import { parseSourceFile } from "./parser";
import type { SourceFile } from "./parser";

// ...

let file: SourceFile;
try {
    file = parseSourceFile(filePath);
}
catch {
    // ...
}

To support import or export statements containing both type and value bindings, a user can express which bindings are type-only by prefixing them with the type keyword.

import { parseSourceFile, type SourceFile } from "./parser";

A major difference with the above is that such imports are retained even if all bindings are declared type-only.

// This still imports "./parser" at runtime.
import { type SourceFile } from "./parser";

Type Assertions

Type systems do not have perfect information about the runtime type of an expression. In some cases, they will need to be informed of a more-appropriate type at a given position. Type assertions - sometimes called casts - are a means of asserting an expression's static type.

const point = JSON.parse(serializedPoint) as ({ x: number, y: number });

The term "type assertion" was chosen in TypeScript to distance from the idea of a "cast" which often has runtime implications. In contrast, type assertions have no runtime behavior.

Non-Nullable Assertions

One of the most common type-assertions in TypeScript is the non-null assertion operator. It convinces the type-checker that a value is neither null nor undefined. For example, one can write x!.foo to specify that x cannot be null nor undefined.

// assert that we have a valid 'HTMLElement', not 'HTMLElement | null'
document.getElementById("entry")!.innerText = "...";

In TypeScript, the non-null assertion operator has no runtime semantics, and this proposal would specify it similarly; however, there is a case for adding non-nullable assertions as a runtime operator instead. If a runtime operator is preferred, that would likely become an independent proposal.

Generics

In modern type systems, it's not enough to just talk about having an Array - often, we're interested in what's in the Array (e.g. whether we have an Array of strings). Generics give us a way to talk about things like containers over types, and the way we talk about an Array of strings is by writing Array<string>.

Like everything else in this proposal, generics have no runtime behavior and would be ignored by a JavaScript runtime.

Generic Declarations

Generic type parameters can appear on type and interface declarations. They must start with a < after the identifier and end with a >:

type Foo<T> = T[]

interface Bar<T> {
    x: T;
}

Functions and classes can also have type parameters, though variables and parameters cannot.

function foo<T>(x: T) {
    return x;
}

class Box<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

Generic Invocations

One can explicitly specify the type arguments of a generic function invocation or generic class instantiation in TypeScript.

// TypeScript
add<number>(4, 5)
new Point<bigint>(4n, 5n)

The above syntax is already valid JavaScript that users may rely on, so we cannot use this syntax as-is. We expect some form of new syntax that could be used to resolve this ambiguity. No specific solution is proposed at this point of time, but one example option is to use a syntactic prefix such as ::

// Types Annotations - example syntax solution
add::<number>(4, 5)
new Point::<bigint>(4n, 5n)

These type arguments (::<type>) would be ignored by the JavaScript runtime. It would be reasonable for this non-ambiguous syntax to be adopted in TypeScript as well.

this Parameters

A function can have a parameter named this as the first parameter, and this parameter (and its type) is ignored by the runtime. It has no effect on the length property of the function, and does not impact values like arguments.

function sum(this: SomeType, x: number, y: number) {
    // ...
}

It would be expected that using a this parameter in an arrow function would either be disallowed by the grammar, or trigger an early error.

// Error!
const oops = (this: SomeType) => {
    // ...
};

Intentional Omissions

We consider the following items explicitly excluded from the scope of this proposal.

Omitted: TypeScript-specific features that generate code

Some constructs in TypeScript are not supported by this proposal because they have runtime semantics, generating JavaScript code rather than simply being stripped out and ignored. These constructs are not in the scope of this proposal, but could be added by separate TC39 proposals.

Omitted: JSX

JSX is an XML-like syntax extension to JavaScript that is designed to be transformed by a pre-processor into valid JavaScript. It is widely used in the React ecosystem, but it has been used for different libraries and frameworks. Because JSX directly interleaves with JavaScript code, compilers and type-checkers for JavaScript typically also support checking and transforming JSX as well. Some users may hope that the JSX transform could also be directly supported by ECMAScript, to expand the set of use-cases that can be handled without a build step.

We do not consider JSX to be in scope of this proposal because:

  • JSX is an orthogonal feature that is independent of optional static types. This proposal does not affect the viability of introducing JSX into ECMAScript via an independent proposal.
  • JSX syntax expands into meaningful JavaScript code when transformed. This proposal is only concerned with syntax erasure.

Up For Debate

A couple pieces of syntax would fit cleanly in with the "types as comments" model, but may feel like a bit of overreach. We think that this proposal could work with or without these pieces of syntax, and in some cases present various alternative options below.

Ambient Declarations

Occasionally it is necessary to inform type-checkers that certain values exist, or even that modules exist. Type systems have what are called ambient declarations, where a declaration will omit its implementation. While type systems typically have their own "declaration file" formats just for declarations like these (e.g. .d.ts files in TypeScript, .flow.js in Flow, etc.), it can be convenient to declare these values in an implementation file (i.e. a .js file) as well.

In type systems today, a declare keyword can precede bindings such as variable, function, and class declarations.

declare let x: string;

declare class Foo {
    bar(x: number): void;
}

declare function foo(): boolean;

At the moment, this proposal does not reserve space for ambient declarations, but it is an option.

Function Overloads

In existing type systems, function/method declarations may omit their body. This is used for function overloading, which communicates that the return type of a function varies with its inputs.

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 + "!"
    }
}

At the moment, this proposal does not reserve space for overload declarations, but it is an option.

Class and Field Modifiers

Several keywords in TypeScript and other type systems are used outside of a type context:

  • abstract classes and methods
  • private, protected and public placement of fields and methods
  • readonly fields
  • override fields and methods

As an example, this proposal could support the following syntax:

class Point {
    public readonly x: number
}

If these are permitted as part of this proposal, the semantics would be to ignore them all, treating the above class the same as the following.

class Point {
    x;
}

As with types, these are "soft guarantees" that are not enforced at runtime, but are checked by the type checker. There would likely need to be slightly different syntax to prohibit newlines after these new contextual keywords.

It might feel strange to include this set of keywords and permit them to be used "incorrectly". Additionally, there may be more keywords added over time (e.g. override most recently).

As one way to achieve the same functionality, existing type systems could find a compromise between type annotation syntax and existing comment syntax. For example, TypeScript already supports some of these modifiers in JSDoc.

class Point {
  /**
   * @public
   * @readonly
   */
  x: number
}

There might be another direction in which this proposal expands comment syntax just to support modifiers like these, which lead with a specific sigil.

class Point {
  %public %readonly x: number

  @@public @@readonly x: number
}

The above ideas try to strike a balance between not overreaching in syntax, while also achieving maximal compatibility with existing type systems. We're open to any of the four solutions presented here, or other ideas people may have.

FAQ

Does JavaScript need static type-checking?

Given how much effort organizations and teams have put into building type-checkers and adopting them, the answer is yes. Perhaps not every developer will reach for static type-checking, and that's okay - that's why this proposal makes type annotations entirely optional; however, the ecosystem demand for using types is undeniable.

TypeScript has done a great job of demonstrating this - it's gained very widespread use with broad signals that people want to keep using it. It's opt-in, but it has a major presence in the ecosystem, and these days TypeScript support is seen as a huge advantage for libraries.

The question is not whether JS should have types, but rather "how should JS work with types?" One valid answer is that the current ecosystem provides sufficient support where types are stripped out separately ahead-of-time, but this proposal may provide advantages over that approach.

Why not define a type system for JS in TC39 instead?

TC39 has a tradition of programming language design which favors local, sound checks. By contrast, TypeScript's model -- which has been highly successful for JS developers -- is around non-local, best-effort checks. TypeScript-style systems are expensive to check at application startup, and would be redundant every time you run your JavaScript applications.

Additionally, defining a type system to run directly in the browser means that improved type analyses would become breaking changes for the users of JavaScript applications, rather than for developers. This would violate goals around web compatibility (i.e. "don't break the web"), so type system innovation would become near-impossible. Allowing other type systems to analyze code separately provides developers with choice, innovation, and freedom for developers to opt-out of checking at any time.

In contrast, trying to add a full type system to JavaScript would be an enormous multi-year effort that would likely never reach consensus. This proposal recognizes that fact, and also recognizes that the community has evolved type systems that it is already happy with.

How does this proposal relate to TypeScript?

This proposal is a balancing act: trying to be as TypeScript compatible as possible while still allowing other type systems, and also not impeding the evolution of JavaScript's syntax too much. We acknowledge that full compatibility is not within scope, but we will strive to maximize compatibility and minimize differences.

This proposal will require work from both ECMAScript and TypeScript itself, where ECMAScript expands its current syntax, but TypeScript makes certain concessions so that types can fit into that space. As mentioned elsewhere, some constructs (like enums and namespaces) have been set aside with the option to propose them separately in TC39. Others are still up for discussion (like class modifiers and ambient declarations). And for others, there will need to be work to disambiguate current code (like arrow functions).

TypeScript would continue to exist alongside JavaScript's slightly more restricted type syntax. In other words, all existing TypeScript projects would continue to compile, and no existing TypeScript codebase would need to change. But taking any existing TypeScript code that lives outside of JavaScript's type syntax would not run - it would still need to be compiled away.

Should TypeScript be standardized in TC39?

TypeScript has continued to evolve very quickly in both syntax and type analyses, to the benefit of users. Tying this evolution to TC39 risks holding that benefit back. For example, TypeScript upgrades often require users to fix type issues because the rules change, and this is often considered "worth it" because real bugs are found; however, this version upgrade path wouldn't be viable for the ECMAScript standard. A move to standardization would require more conservatism. The goal here is to enable wider deployment of systems like TypeScript in diverse environments, not obstruct TypeScript's evolution.

Should TypeScript be sanctioned as JS's official type system?

It's unclear what it would mean for there to be an "official" type system for JavaScript. For example, there is no "official" linter or "official" formatter for JavaScript. Instead there are tools that grow in popularity and evolve over time.

Additionally, type checkers, such as Flow, Closure, and Hegel may wish to use this proposal to enable developers to use their type checkers to check their code. Making this proposal be only about TypeScript can hamper this effort. Amicable competition in this space would be beneficial to JavaScript as it can enable experimentation and new ideas.

Why not unofficially build TS checking and compilation into various systems?

A number of systems, such as ts-node and deno, have tried this. Apart from startup performance issues, a common problem is compatibility across versions and modes, in type checking semantics, grammar, and transpilation output. This proposal would not subsume the needs for all of those features, but it would provide one compatible syntax and semantics to unify around for many needs.

Why not stick to existing JS comment syntax?

Although it is possible to define types in existing JavaScript comments, as Closure and TypeScript's JSDoc mode do, this syntax is much more verbose and unergonomic.

Consider the fact that JSDoc type annotations were present in Closure Compiler prior to TypeScript, and that TypeScript's JSDoc support has been present for years now. While types in JSDoc has grown over the years, most type-checked JavaScript is still written in TypeScript files with TypeScript syntax. This is similarly the case with Flow, where Flow's type-checker can analyze Flow-like syntax in existing JavaScript comments, but most Flow users continue to just use direct annotation/declaration syntax.

Don't all JS developers transpile anyway? Will it really help to remove the type-desugaring step?

The JavaScript ecosystem has been slowly moving back to a transpilation-less future. The sunsetting of IE11 and the rise of evergreen browsers that implement the latest JavaScript standard means that developers can once again run standard JavaScript code without transpilation. The advent of native ES modules in the browser and in Node.js also means that, at least in development, the ecosystem is working its way to a future where even bundling is not necessary. Node.js developers in particular, have historically avoided transpilation, and are today torn between the ease of development that is brought by no transpilation, and the ease of development that languages like TypeScript bring.

Implementing this proposal means that we can add type systems to this list of "things that don't need transpilation anymore" and bring us closer to a world where transpilation is optional and not a necessity.

Can types be available via runtime reflection like TypeScript's emitDecoratorMetadata?

The proposal here differs significantly from Python's types, as the types in this proposal are entirely ignored, not evaluated as expressions or accessible at runtime as metadata.

This proposal explicitly does not take a stance on runtime reflection leaving further work for future proposals. The main reason is that this proposal does not block further work in this space but rather enables it. This is also the route Python took when adding their types to the language.

Users who rely on decorator metadata could continue to leverage a build step as desired.

Does this proposal make all TypeScript programs valid JavaScript?

Most constructs in TypeScript are compatible, but not all, and most of those that do not pass can be converted via simple codemod changes that can make them both TypeScript-compatible and compatible with this proposal.

See the "up for debate" and "Intentional Omissions" sections for more information.

Does this proposal make all Flow programs valid JavaScript?

Flow is very similar to TypeScript, and so most type constructs would work, with a similar caveat in which some types might need to be wrapped in parentheses to be compatible with this proposal.

Two constructs that do not conform to this proposal are type casting (e.g. (x: number)) and opaque type aliases (e.g. opaque type Meters = number). Flow could consider modifying these in the language so that they conform to this proposal, e.g. adopt the as operator as an alternative to (x: number), and type Meters = (new number). This proposal could also reserve room for opaque type aliases.

What about .d.ts files and "libdef" files?

Declaration files and library definition files are used by TypeScript and Flow respectively as a kind of "header" file that describes values, their types, and where they exist. This proposal can safely ignore them as it does not need to interpret the semantics of the type information inside them. TypeScript and Flow will continue reading and analyzing these files as they do today.

Does this proposal mean that TypeScript developers would have to modify their codebases?

No. TypeScript can continue to be TypeScript, with no compatibility impact or changes to codebases. This proposal would give developers the option to restrict themselves to a particular subset of TypeScript which would run as JavaScript without transpilation.

Developers may still want to use TypeScript syntax for other reasons:

  • Use of certain syntax features which are not supported in JavaScript (e.g., enums and parameter properties)
  • Compatibility with existing codebases which may run into certain syntax edge cases that are handled differently
  • Non-standard extensions/reinterpretations of JavaScript (e.g. experimental legacy decorators, decorator metadata, or [[Set]] semantics for fields)

If developers decide to migrate an existing TypeScript codebase to use the JavaScript syntax specified in this proposal, the goal of this proposal is that the modifications would be minimal. We believe that many developers would be motivated by removing a build step, but others could decide to stick with TypeScript compilation and enjoy the full power of the language.

How should tools work with JavaScript type syntax?

Given the fact that some TypeScript features are out of scope, and that standard JavaScript will not evolve as fast as TypeScript or support its variety of configurations, there will continue to be an advantage for many tools to support TypeScript in its fuller form, beyond what is potentially standardized as JavaScript.

Some tools currently need a "plugin" or option to get TypeScript support working. This proposal would mean that these tools could support type syntax by default, forming a standard, versionless, always-on common base for type syntax. Full and prescriptive support for TypeScript, Flow, etc. could remain an opt-in mode on top of that.

What about compatibility with ReasonML, PureScript, and other statically typed languages that compile to JavaScript?

While these languages compile to JavaScript, and have static typing, they are not supersets of JavaScript, and thus are not relevant to this proposal.

Will the ability to deploy typed source code directly result in bloated applications?

Returning to a world where code does not strictly need to be compiled prior to being used in production means that developers may end up deploying more code than is necessary. Hence larger payloads over-the-wire for remotely served apps, and more text to parse at load time.

However this situation already exists. Today, many users omit a build step and ship large amounts of comments and other extraneous information, e.g. unminified code. It remains a best practice to perform an ahead-of-time optimization step on code destined for production if the use-case is performance-sensitive.

Specifically, note that the TypeScript compiler tsc does not have a built-in option to minify. Allowing TypeScript users to skip tsc during development will not inherently encourage users to skip a minification step, since minifying TypeScript would have required a separate action in any case.

Babel is another popular transpiler for TypeScript, Flow, and Hegel. Babel's preset-typescript and preset-flow transpilers do not minify. In Babel, minification requires a separate step (usually performed by a bundler). Allowing Babel users to omit preset-typescript will not encourage users to skip minification.

In this proposal, type annotations are treated as comments. Making comments more useful and easier to use is an appropriate evolution for JavaScript, even when we expect developers to remove comments and type annotations via minification.

Are unchecked types a footgun?

This proposal introduces type annotations that are explicitly not checked at runtime. This is intentional to minimize the runtime cost of the annotations and to provide a consistent mental model in which the types do not affect program behavior. A potential risk is that users might not realize the need to run an external tool to find type errors, and consequently are surprised when type-related bugs arise in their type-annotated code.

For today's users of external type-checkers, this risk already exists. Users mitigate this risk today via a combination of:

  • IDEs that perform type checking and proactively surface type errors
  • Integration of type checking into project development workflows, e.g. npm scripts, CI systems, tsc

In some ways it would be more of a surprise to users if the types were runtime-checked. It is common in other languages for there to be minimal runtime type-checking. For example, in C++ there is almost no checking at runtime except for some known cases such as when the programmer requests it, e.g. dynamic_cast.

As seen with TypeScript, developers quickly learn that the types play no role at runtime. Therefore similar to the first question of "Does JavaScript need a static type system?", this question has been answered, in part, by the widespread success of external type-checkers.

How could runtime-checked types be added in future?

It seems unlikely that JavaScript would adopt a pervasive runtime-checked type system due to the runtime performance overhead that it would bring. While it's possible that there may have been room for improvement, past efforts such as TS* (Swamy et al) have shown that runtime type-checking based on annotations adds a non-negligible slowdown. That is one reason why this proposal endorses purely static types.

If there is a desire to keep language open to later adding runtime-checked types, in addition to the static types proposed here, we could make an explicit syntax reservation in the grammar to support both.

Could this proposal enable runtimes to optimize performance based on type hints?

Whilst it is possible to theorize runtime optimizations driven by statically declared types, the proposal authors are not aware of successful experiments in JavaScript that meaningfully beat dynamic type-driven JIT optimization.

The proposed type syntax has no defined semantics and so is opaque to the runtimes. It is equivalent to asking "Could a runtime use /** comments **/ to optimize performance?" To which the answer is: almost certainly not - at least not in a standard way. Therefore this proposal alone does not directly offer new opportunities for performance improvements.

It is explicitly a non-goal of this proposal to improve the performance of JavaScript.

Prior Art

Other languages that have optional erasable type syntax

When Python decided to add a gradual type system to the language, it did it in two steps. First, a proposal for type annotations was created in PEP-3107 - Function Annotations that specified parameter types and function return types in Python Several years later, the places in which types could occur was expanded in Python PEP-484 - Type Hints.

The proposal here differs significantly from Python's types, as the types in this proposal are entirely ignored, not evaluated as expressions or accessible at runtime as metadata. This difference is largely motivated by the existing community precedent, where JS type systems do not tend to use JS expression grammar for their types, so it is not possible to evaluate them as such.

Ruby 3 has now also implemented RBS: type definitions that sit beside the code and are not part of it.

Languages that add type systems onto JavaScript

TypeScript, Flow, and Hegel are languages that implement type systems above standard JavaScript.

Ability to add type systems to JavaScript via comments

Both TypeScript and Flow enable developers to write JavaScript code and incorporate types annotations that the JavaScript runtime ignores.

For Flow, these are Flow comment types, and for TypeScript these are JSDoc comments.

See the author's blog post on their positive experience with TypeScript's JSDoc comments here.

Closure Compiler's type checking works entirely via JSDoc comments (docs). The Closure Compiler team has received many requests for an in-line type syntax, but was hesitant to do this without a standard.

Relevant proposals and discussions in TC39

The earliest proposal for types in JavaScript that we're aware of is Waldemar Horwat's "Types" specification from July 2000.

The proposed draft of ECMAScript 4th edition in 2008 called for types and optional type annotations; TC39 agreed to withdraw the ES4 proposal in 2008, shipping ES 3.1 as ES5 in 2010. Auth0 has a blog post with a brief history of the saga of ECMAScript 4.

TC39 has previously discussed guards, which form a new, stronger type system.

Previously, Sam Goto led discussions around an optional types proposal which aimed to unify syntax and semantics across the type-checkers. Trying to find agreement across type-checkers, along with defining a sufficient subset in both syntax and semantics meant that there were difficulties with this approach. An evolution of this plan was pluggable types which was inspired by Gilad Bracha's ideas on pluggable type systems. This proposal is extremely similar to the pluggable types proposal, but leans a bit more heavily on the idea of viewing types as comments, and comes at a time with broader adoption of type-checking and a more mature type-checking ecosystem.

In 2015, Google's V8 team experimented with a proposal to implement a new JS mode that they called "Strong Mode," intending to use types to improve site performance. Google canceled the experiment in failure. "In the end we had to give up on this one."

The optional types proposal repository contains links to other prior discussions around types in JavaScript:

Relevant discussions elsewhere

Gilad Bracha has advocated for pluggable type systems.

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,534
star
4

proposal-pattern-matching

Pattern matching syntax for ECMAScript
HTML
5,498
star
5

proposal-optional-chaining

HTML
4,942
star
6

proposal-signals

A proposal to add signals to JavaScript.
3,387
star
7

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,321
star
8

proposal-observable

Observables for ECMAScript
JavaScript
3,058
star
9

proposal-decorators

Decorators for ES6 classes
2,640
star
10

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,496
star
11

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
12

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,863
star
13

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,742
star
14

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,722
star
15

proposal-async-await

Async/await for ECMAScript
HTML
1,578
star
16

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,493
star
17

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,429
star
18

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,307
star
19

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,232
star
20

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,083
star
21

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
22

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
23

proposal-binary-ast

Binary AST proposal for ECMAScript
961
star
24

agendas

TC39 meeting agendas
JavaScript
952
star
25

proposal-built-in-modules

HTML
891
star
26

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
857
star
27

proposal-explicit-resource-management

ECMAScript Explicit Resource Management
JavaScript
746
star
28

proposal-set-methods

Proposal for new Set methods in JS
HTML
655
star
29

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
614
star
30

proposal-operator-overloading

JavaScript
610
star
31

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
591
star
32

proposal-async-context

Async Context for JavaScript
HTML
587
star
33

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
561
star
34

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
540
star
35

ecma402

Status, process, and documents for ECMA 402
HTML
529
star
36

proposal-slice-notation

HTML
523
star
37

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
511
star
38

notes

TC39 meeting notes
JavaScript
496
star
39

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
40

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
483
star
41

proposal-decimal

Built-in exact decimal numbers for JavaScript
HTML
477
star
42

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
463
star
43

proposal-module-expressions

HTML
433
star
44

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
45

proposal-UnambiguousJavaScriptGrammar

413
star
46

proposal-weakrefs

WeakRefs
HTML
409
star
47

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
48

proposal-error-cause

TC39 proposal for accumulating errors
HTML
380
star
49

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
50

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
374
star
51

proposal-module-declarations

JavaScript Module Declarations
HTML
369
star
52

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
352
star
53

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
54

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
55

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
345
star
56

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
330
star
57

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
319
star
58

tc39.github.io

Get involved in specifying JavaScript
HTML
318
star
59

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
318
star
60

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
61

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
310
star
62

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
63

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
306
star
64

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
65

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
279
star
66

proposal-json-modules

Proposal to import JSON files as modules
HTML
272
star
67

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
270
star
68

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
267
star
69

proposal-promise-with-resolvers

HTML
255
star
70

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
253
star
71

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
242
star
72

proposal-structs

JavaScript Structs: Fixed Layout Objects
230
star
73

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
223
star
74

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
75

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
214
star
76

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
214
star
77

proposal-defer-import-eval

A proposal for introducing a way to defer evaluate of a module
HTML
208
star
78

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
79

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
200
star
80

proposal-optional-chaining-assignment

`a?.b = c` proposal
186
star
81

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
82

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
83

proposal-array-from-async

Draft specification for a proposed Array.fromAsync method in JavaScript.
HTML
178
star
84

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
176
star
85

proposal-collection-methods

HTML
171
star
86

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
87

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
169
star
88

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
89

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
166
star
90

proposal-intl-duration-format

164
star
91

how-we-work

Documentation of how TC39 operates and how to participate
161
star
92

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
93

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
94

proposal-extensions

Extensions proposal for ECMAScript
HTML
150
star
95

proposal-hashbang

#! for JS
HTML
148
star
96

proposal-import-meta

import.meta proposal for JavaScript
HTML
146
star
97

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
146
star
98

proposal-resizablearraybuffer

Proposal for resizable array buffers
HTML
145
star
99

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
100

eshost

A uniform wrapper around a multitude of ECMAScript hosts. CLI: https://github.com/bterlson/eshost-cli
JavaScript
142
star