Skip to content

type syntax = expression syntax #206

Open

Description

@phaux

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
// etc

Object 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:

  • void is an unary operator - could be replaced with Void
  • T[] would be invalid syntax - just use Array.<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) or infer.<T>
  • T extends U - extends(T, U) or extends.<T, U> or T instanceof U
  • etc

Activity

  1. azder commented on Dec 20, 2023

    @azder

    not favor any particular existing type checker

    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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions