KopScript logo

KopScript

KopScript is a small, strongly-typed, object-oriented language that transpiles to readable JavaScript — leaning toward C# paradigms (type-first declarations, interfaces, explicit virtual/override dispatch, auto-properties) rather than TypeScript ones. Published as kopscript on npm.

Type-first, C#-flavored classes

The type comes before the name, everywhere — variables, function parameters and return types, class members. Methods are sealed by default, like C#; a subclass overrides one only if the base class explicitly marked it virtual.

class Animal {
  public string Name { get; set; }
  constructor(string name) { this.Name = name; }
  public virtual string Speak() { return this.Name + " makes a sound"; }
}

class Dog : Animal {
  public override string Speak() { return this.Name + " barks"; }
}

Control flow, and error handling

Ordinary if/else, while, C-style for, and a typed foreach — plus try/catch/finally for a rejected task (or a plain throw) to be caught, matching real JS exception flow underneath.

for (number i = 0; i < 3; i = i + 1) { print(i); }
foreach (number x in [1, 2, 3]) { print(x); }

try {
  throw "boom";
} catch (string message) {
  print("caught: " + message);
} finally {
  print("done");
}

throw accepts any expression, and a catch parameter's type is your choice — it's not checked against what was actually thrown, matching JS's own untyped catch underneath.

Modules

using "./path"; brings every public top-level declaration from that file into scope — no namespace prefix, the same way a C# using directive works. It only resolves relative paths within a project; there's no package-import mechanism yet, which is why cross-repo consumption (see the Framework docs) goes through extern instead.

extern — JS/npm interop

extern declarations describe the shape of something that already exists in JS, the same trust model as a TypeScript .d.ts file. A virtual method on an extern class lets a real KopScript class override it — which is what lets a class subclass something from an entirely different package.

extern class Component {
  constructor();
  virtual VElement Render();
} from "kopular/component";

class MyWidget : Component {
  public override VElement Render() { ... }
}

A related, narrower form embeds a real file's contents as a compile-time string constant — no extern, no runtime file read: raw string HeroHtml from "./hero.html";. This very site's markup (this page included) is built that way.

async/await, task<T>

task/task<T> are the one hardcoded parametrized type outside state<T> — not general generics, just enough to give async/await a return type. Both compile directly to real async function and await.

async task<number> Double(number x) {
  return x * 2;
}

state<T> — reactive state, no RxJS

A small reactive box: .Value get/set, .Subscribe((T) => void) firing on every assignment. No Observables, no operators, no manual unsubscribe bookkeeping — this is what Kopular's Component is built around.

state<number> count = state(0);
count.Subscribe((number v) => print("now " + v));
count.Value = count.Value + 1; // prints "now 1"

Nullable types — T?

T? is a value of type T, or null — erased at compile time (JS already has native null), but enforced by the checker: a T? can't be used where a T is expected without a null check first, and comparing a value whose type can't be null to null is itself a compile error, not a silently-always-false comparison.

void Greet(string? name) {
  if (name != null) {
    print("Hello, " + name);   // name is `string` here, not `string?`
  } else {
    print("Hello, stranger");
  }
}

Narrowing also works inside &&/|| short-circuits (x != null && x.Length > 0), but is deliberately scoped to a bare local/parameter checked directly in an if — no control-flow analysis for an early-return guard clause, no narrowing through a this.Field path.

Generics

Classes and interfaces take a single, unconstrained, invariant type parameter — erased at compile time (like task<T>/state<T> above), so Box<number> and Box<string> compile to the exact same plain class with zero runtime cost.

class Box<T> {
  public T Value;
  constructor(T v) { this.Value = v; }
  public T Get() { return this.Value; }
}

Box<number> nb = new Box<number>(5);
Box<string> sb = new Box<string>("hi");

The type argument is required everywhere — on the declared type and on new — and it's invariant: Box<Dog> is not assignable to Box<Animal> even if Dog : Animal. Deliberately just this much: no multiple type parameters (Map<K, V>), no constraints (T : IFoo), no generic functions, no generic inheritance — each a real, separate extension rather than a v1 oversight.

An extern class can carry the same <T>, so a generic type from another package instantiates and type-checks exactly like a local one:

extern class FormField<T> {
  constructor(T initial, (T) => string? validate);
  state<T> Value;
} from "kopular/forms";

FormField<string> email = new FormField<string>("", (string v) => null);

Also in the box

Interfaces (with inheritance, and their own single type parameter), enums, string interpolation ($"Hello, {name}!"), a match expression with literal/regex patterns, lambdas and real closures, and a small PascalCase stdlib on strings and arrays (.Map, .Filter, .ForEach, non-mutating .Push).