Skip to content

Latest commit

 

History

History
159 lines (116 loc) · 9.05 KB

type-annotations.md

File metadata and controls

159 lines (116 loc) · 9.05 KB

Type Annotations

GocciaScript's TypeScript-compatible type syntax: the TC39 Type Annotations proposal, the --strict-types runtime extension, and the parsing rules that govern them.

Executive Summary

  • Types as comments by default — Supported annotations follow the TC39 Type Annotations proposal's types-as-comments model and have no runtime effect
  • Optional enforcement — --strict-types checks supported annotations at runtime; it is a runtime contract, not a static structural checker such as tsc
  • Extension decides < — A leading < is read as JSX or as a type parameter list by file extension, modeled on TypeScript with GocciaScript-specific extension rules (JSX is also recognized in .js and .mjs, and never in .ts or .mts)
  • Restricted ! productions — Definite assignment and non-null assertions must sit on the same line as their operand, so ASI code keeps its usual meaning
  • Bounded surface — Namespaces, parameter properties, angle-bracket assertions, class-field definite assignment, and generic async arrows are deliberately unsupported

The annotation surface is part of GocciaScript's language profile; see Language for the ECMAScript baseline it sits on and Language Tables for the proposal-status matrix.

Proposal and Runtime Model

GocciaScript implements the TC39 Type Annotations proposal. Supported annotations follow its types-as-comments model and have no runtime effect by default. Raw type strings are preserved on AST nodes for potential future optimization.

GocciaScript runtime extension — pass --strict-types (or set "strict-types": true in goccia.json) to enforce supported annotations at runtime. Annotated variables, function parameters, and literal-inferred types are checked on initial value and on every assignment; incompatible values throw TypeError. Union (string | number), any, and unknown annotations remain unenforced. This optional runtime contract is not a replacement for a static structural checker such as tsc.

An unannotated let takes an inferred type from the syntactic form of its initializer alone, and only from these forms:

Initializer Inferred type
Number, string, or boolean literal number, string, or boolean
Template literal, with or without substitutions string
Object literal, array literal, new expression, arrow function, or function expression object (any non-primitive value)

Every other initializer leaves the binding untyped, whatever the engine could work out about its value: an identifier, a call, an operator expression (including a negated number such as -1), a BigInt literal, null, or undefined.

const base = 16;
let count = 1;        // inferred number: count = "text" throws TypeError
let total = base + 1; // untyped: total = "text" is allowed

Supported Syntax

// Variable type annotations
let x: number = 42;
const name: string = "hello";
let value: string | number = "test";

// Parameter type annotations (simple, optional, rest, destructuring)
const add = (a: number, b: number): number => a + b;
const greet = (name?: string) => name === undefined ? "hi" : "hi " + name;
const sum = (...nums: number[]) => nums.reduce((a, b) => a + b, 0);
const first = ({ name, age }: { name: string, age: number }) => name;

// Return type annotations, including unions and intersections of object types
const double = (x: number): number => x * 2;
class Registry {
  lookup(flag: boolean): string | { code: number } { return flag ? "ok" : { code: 7 }; }
  build(): { name: string } & { size: number } { return { name: "box", size: 2 }; }
}

// Template literal types
const id: `id-${number}` = "id-42";
const also = "id-42" as `id-${number}`;

// Definite assignment assertions (require an annotation, forbid an initializer)
let pending!: number;
pending = 5;

// Non-null assertions (postfix !, erased — the chain continues unchanged)
const nonNull = { v: "x" };
nonNull.v!.length;
const firstY = [{ y: 1 }][0]!.y;

// Generic arrow function expressions
const identity = <T,>(v: T): T => v;
const withDefault = <T = string,>(v: T): T => v;

// Type and interface declarations (skipped entirely)
type Point = { x: number, y: number };
interface Animal { name: string; speak(): string; }

// import type / export type (skipped entirely)
import type { Foo } from './types.js';
export type { Bar };

// Mixed type/value named bindings keep runtime imports/exports for value bindings
import { parseSourceFile, type SourceFile } from "./parser.js";
export { value, type ValueShape };

// export interface declarations are skipped entirely
export interface Serializable {
  toJSON(): string;
}

// Runtime-erased as, satisfies, and generic constructor syntax
const asserted = 42 as number;
const colors = ["red", "green"] as const satisfies Array<string>;
const names = new Map<string, string>();

// Class annotations: field types, generics, implements, access modifiers
class Box<T> implements Container {
  public value: T;
  private label?: string;
  readonly id: number = 1;
  constructor(value: T) { this.value = value; }
  get(): T { return this.value; }
}

// Catch parameter type annotation
try { throw new Error("oops"); } catch (e: Error) { }

Angle Brackets and JSX

A leading < is ambiguous: it can open a JSX element or a type parameter list. GocciaScript resolves this by file extension — modeled on TypeScript's .ts/.mts versus .tsx rule, with GocciaScript-specific extensions: JSX is also recognized (with a warning) in .js and .mjs sources.

  • .ts, .mts — JSX is never recognised, so < is always type syntax. Generic function type annotations parse here, as does every supported generic arrow form — including the bare <T> the JSX extensions cannot take. Generic async arrows are unsupported in every extension; see Not Supported.

    const identity: <T>(x: T) => T = (x) => x;
    const wrap = <T>(v: T): T[] => [v];
    const idOf = <T extends { id: number }>(v: T): number => v.id;
  • .js, .jsx, .tsx, .mjs — JSX is recognised, so a bare <T> or <T extends U> is read as a JSX element. Generic arrows still parse when the type parameter list cannot be mistaken for a tag, which means a trailing comma, a default, or more than one parameter:

    const identity = <T,>(v: T): T => v;
    const withDefault = <T = string,>(v: T): T => v;
    const pair = <A, B>(a: A, b: B) => [a, b];

    Use a .ts or .mts source when an annotation needs the bare <T> form.

Arrow Return Types in Conditionals

Inside a conditional's consequent, (x) : y => z reads two ways: an arrow with return type y, or the parenthesized expression (x), the conditional's :, and an alternate arrow y => z. GocciaScript follows TypeScript's rule: there, the return-typed arrow is kept only when the : that closes the conditional follows its body. One : is needed for each consequent the arrow is nested in. Otherwise the group is a parenthesized expression, so plain JavaScript such as minified c ? (l) => ({ a: 1 }) : (l) => 2 parses as written.

const pick = c ? (x: number): number => x : (y: number) => -y; // return-typed arrow
const make = c ? (l) => ({ a: 1 }) : (l) => 2;                 // `({ a: 1 })` is the body

Automatic Semicolon Insertion and Type Syntax

Type-level declarations that are skipped rather than parsed (type, import type, export type) are erased whole under ASI, even when they span lines: a union | or intersection & leading the next line, a break right after the =, a break before a function type's =>, and a conditional type broken around ? and : all stay part of the declaration. A type argument list wrapped across lines is fine too, because its breaks fall after a < or , or before a >.

A type query keeps its operand across a line break the same way an operator does, so let value: typeof followed by source on the next line reads as one annotation.

This only applies when ASI is enabled; see Language for the opt-in flag and the general rules.

Both ! forms are restricted productions: the ! must appear on the same line as the binding name (definite assignment) or its operand (non-null assertion). A ! at the start of the next line begins a new expression statement, so ASI code such as let x followed by !fn() keeps its usual meaning. != and !== lex as single tokens and are never assertions.

Not Supported

  • Namespaces (namespace Foo { ... }).
  • Parameter properties in constructors (constructor(public x: number)).
  • Angle-bracket type assertions (<string>value) — use value as string instead.
  • Definite assignment assertions on class fields (class C { x!: number }) — only variable declarations accept !.
  • Generic async arrow expressions (async <T,>(v: T) => v) — the generic form is wired into primary-expression position only, so the async prefix is not recognised ahead of it.