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

Suggestion: using string literals for type annotations #186

Open SheepTester opened 1 year ago

SheepTester commented 1 year ago

Adding fuel to the fire that is the other syntax bikeshedding proposals, I propose that type annotations be instead limited to string literals. Something like this, though for the reasons listed below, the string contents ultimately don't matter.

let x: 'string';

'type Pair<T> = [T, T]';

`interface Zippable<T> {
  zip(a: T, b: T): T where this: T;
}`;

function foo(x: 'Pair<T>', y: '? Pair<T>'): 'Pair<T> where T extends Zippable<number>' {
  // ...
}

'function bar(x: number)';
'function bar()';
function bar() {}

var hello = JSON.parse('"hello"') as 'string';
let hi = 'hi' as 'const';

To be clear: you can put anything in these strings, and it wouldn't be a syntax error. The way I formatted types in the examples above isn't the focus of my proposal.

const hello: 'number[' = ["Yes, that's an unclosed bracket. That's a problem for your IDE, not the browser."];
function hi(): '\0🤪' {}

This doesn't cover all the syntax that TypeScript offers, such as a!.b or generic invocations, but I don't think that was the aim of this proposal. Besides, this offers enough flexibility that type checkers can invent their own syntax, like (a as '!null').b or (add as '<number>')(4, 5).

Here are the benefits to string literal type annotations:

  1. Limited scope: It's very unlikely to have backwards compatibility issues or parsing problems (unlike with <...>). Bare string statements are already allowed and ignored in JavaScript, so no special syntax is needed for type and interface statements.
  2. Supports the goal of allowing non-TypeScript type checkers, and it's a lot easier for TypeScript and others to add new features without requiring new syntax.
  3. Avoids further bikeshedding about what features type annotations should support because string literals can contain anything.
  4. It could make it clearer that these are type annotations because the string contents will be ignored at runtime. Their editor's syntax highlighting might not care what the programmer types in the string, which can help the user discover that the string contents don't have an effect on runtime. Supporting syntax outside of a string literal, like var arr: Array<number>;, directly in JavaScript may give the impression that the language gives more support for type annotations than just allowing the syntax.
  5. Forward compatibility: If we ever want to add runtime type checking, this syntax gives it room to expand. For example, syntax like function future(x: instanceof Date) that actually enforces types could still be introduced in the future.
  6. Possibly faster parsing. Supposedly, JSON.parsing an object in a string literal is faster than an object literal because object literals require more effort to tokenize. This is probably a baseless claim, but even if this benefit isn't true, it doesn't invalidate the other benefits.
  7. Python already supports something similar (def clone(self) -> 'MyClass':), though in their case it's because they evaluate type expressions at runtime, while this proposal ignores them.
  8. Serializing string literals as a runtime value is trivial. In other words, it's not clear how to represent Array<number> as a JavaScript object at runtime, but 'Array<number>' is just a string. This way, perhaps decorators could access these type annotations at runtime, like Python __doc__. This could be useful for libraries that enforce types at runtime, for example.

And downsides:

  1. Most syntax highlighters, like for blog posts, will probably color the entire type annotation as a string (as they currently do now). Admittedly, there isn't much to color inside a type annotation, so I don't think it's a big deal. I assume IDEs can handle these annotations more intelligently, though.
  2. Autoformatters may not format type annotations if they tokenize them as string literals. Black, an auto-formatter for Python, doesn't format f-strings because they're parsed as string literals, so I'm concerned something similar may happen here.
  3. Nested strings (eg a string literal type let mode: "'on' | 'off'";) or multiline type annotations would be somewhat annoying to write. One convention is to always use template literals; this also allows types to extend across multiple lines. That might introduce extra complexity, though, since I think template literals are parsed differently, and we'd want to disallow expressions like let x: `${a}`;.
  4. This might result in a mess in JavaScript's ecosystem where various packages use different type conventions, but I don't think that's a problem unique to string literal type annotations.
egasimus commented 1 year ago

One step closer to backwards compatible syntax. I like it.

jdannemann commented 1 year ago

For the love of all things holy, please, no...

esummers commented 1 year ago

I'd prefer just putting types in a comment personally.

//: (number, number, number) -> number
function do_something(a, b, c) {
}

//: <T>(T, number, number) -> T
function do_something(a, b, c) {
}

Constructs like Interfaces could be just normal JavaScript objects attached with decorators. They could be inspected at runtime then and if unused be removed at deployment time using tree shaking.

const Foo = new Interface({
...
})

@implements(Foo)
class FooBar {
}
SheepTester commented 1 year ago

I agree, ideally we shouldn't need an ECMAScript proposal for type annotations. I'm personally not a fan of this proposal in general.

Anyone could just make a modern convention using existing syntax. However, without an ECMAScript proposal backing it, we'd just end up with multiple type annotation comment syntaxes, which Python currently also struggles with.

But if the TypeScript team is really intent on adding special syntax for type annotations to the language, then I'd like them to be as unintrusive to the existing grammar as possible.