Repository navigation
type syntax = expression syntax #206
Description
My proposal for including a type syntax with minimal additions to the existing grammar rules.
Goals are:
- reuse as much of existing grammar as possible
- defined at AST level, not token level
- not favor any particular existing type checker
- can have runtime semantics if we decide so later
- future-proof
The idea is: the types are just any expressions, but ignored.
It basically resolves the following issue in the readme:
The challenge with this is denoting the end of a type
[...]
These rules have not yet been established, but will be explored in more detail upon advancement of this proposal
Some restrictions can be added to expressions in type position.
For example a function expression in a type position must not have a block body.
Here are the new syntax additions grouped by use case, the most important ones first (IMHO).
Consuming a typed library
Note how these 3 features cover most of the TypeScript usage when only consuming a typed library.
Type imports
Allow optional type modifier on imports. Fairly obvious.
The imports with the type modifier should be ignored by the runtime.
import type { Ident } from "..."
import { type Ident } from "..."Type annotations
Add optional type annotations on variables, function arguments and function return.
The type after the colon is just an expression and is ignored by the runtime.
let x: Type = 1
let f = (x: Type): Type -> x
function f(arg: Type): Type {}Generic parameter application
In addition to regular function calls (() and ?.()), allow a new syntax .<> (can be replaced with some other syntax e.g. ::<>) for passing generic parameters but ignored during runtime.
This would be valid to use anywhere in expression position, which includes the types, because they're just expressions.
The "arguments" are also just any expression.
let a: Array.<Type> = [...]
a.reduce.<Type>(...)
await new Promise.<Type>(...) // also make sure this is not parsed as `(new Promise()).<Type>()`
class Foo extends Bar.<Type> {}Defining own types
When defining own types, the following features are useful.
Generic parameters in declarations
Allow generic parameters in class and function declarations.
The syntax could be either .<> to be consistent with the application syntax or just <> because it's not ambiguous.
The type parameters could also have a type annotation and default value after them like a regular function argument.
The type annotation of a type parameter could be used to constrain the type parameter (like TS extends).
function foo<Ident>() {}
function foo<Ident: Type>() {}
function foo<Ident = Type>() {}
class Foo<Ident: Type = Type> {}Type declaration syntax
Add a new kind of declaration that declares a type and is ignored by the runtime.
type Ident = Type
type Ident<Ident> = Type
export type Ident = Type
// etcObject optional property
For object literal syntax, allow an optional ? after the property name to indicate that the property is optional.
let o1: { foo: Type, bar?: Type } = { }
let o2 = { a?: 1 } // this has no meaning in TypeScript nor Flow but would be now allowed and ignored+/- could be added too to indicate readonly/writeonly properties like in Flow.
Summary and examples
With just these syntax additions we can accomplish basically everything that typecheckers can do:
import { useState, type MouseEvent } from "react"
type FooProps = {
className?: string
onClick: (e: MouseEvent) => void
}
function Foo(props: FooProps) {
const [state, setState] = useState.<number | null>(null)
return ...
}For incompatible TS/Flow syntax, more syntax additions could be proposed in the future or an alternative JS-expressions-only syntax could be invented:
voidis an unary operator - could be replaced withVoidT[]would be invalid syntax - just useArray.<T>- optional function arguments - just make the type an union with undefined
interface Foo {}- could be replaced with a type-only function:type Foo = interface({}){| x: T |}-strict({ x: T }){ x: T, ... }-{ x: T, ...any }class implements Foo- could stay in the JSDoc realm:/** @implements {Foo} */infer T-infer(T)orinfer.<T>T extends U-extends(T, U)orextends.<T, U>orT instanceof U- etc
Activity
azder commentedon Dec 20, 2023
Just by defining the syntax, you favor at least one type checker. What if the checker doesn't parse TypeScript or Flow like syntaxes, but uses Hindley–Milner one (think of how Haskell does it).
Even now, it is cumbersome to provide TS definitions if your function automatically does partial application, so the syntax changes you propose will not account for code like that, thus make it harder to use (if possible), hence favor particulat existing one(s)