
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.
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"; }
}
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.
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 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.
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;
}
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"
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.
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);
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).