GocciaScript's TypeScript-compatible type syntax: the TC39 Type Annotations proposal, the --strict-types runtime extension, and the parsing rules that govern them.
- 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-typeschecks supported annotations at runtime; it is a runtime contract, not a static structural checker such astsc - 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.jsand.mjs, and never in.tsor.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.
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// 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) { }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
.tsor.mtssource when an annotation needs the bare<T>form.
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 bodyType-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.
- Namespaces (
namespace Foo { ... }). - Parameter properties in constructors (
constructor(public x: number)). - Angle-bracket type assertions (
<string>value) — usevalue as stringinstead. - 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 theasyncprefix is not recognised ahead of it.