Kopular vs. Angular vs. React

A comparison of how Kopular, Angular, and React each approach the same routine work — including where Kopular doesn't win. For measured numbers — runtime performance, bundle size, and how often an AI actually generates each framework correctly on the first try — see the Benchmarks page.

Templates: compiled, not interpreted

All three frameworks let you write markup with data bindings and structural directives — that part of the pitch is genuinely similar now. The difference is what runs it. Angular's *ngFor/*ngIf/{{ }} and React's JSX are each their own syntax, handled by their own separate compiler pass, with their own error messages. Kopular's *for/*if/{{ }} desugar into ordinary KopScript AST nodes before type-checking ever runs — there is no second compiler, no separate expression language inside the bindings, and no runtime template engine at all. A binding expression is real, type-checked KopScript; a mistyped one is the exact same compile error you'd get for that expression anywhere else in the file, reported at its real position in the .html file.

Angular — a template string checked by a separate template compiler, with its own diagnostics:

@Component({
  selector: 'app-list',
  template: `
    <ul>
      <li *ngFor="let item of items">{{ item.name }}</li>
    </ul>
    <p *ngIf="items.length === 0">No items yet.</p>
  `,
})
class ListComponent {
  @Input() items: Item[] = [];
}

React — JSX reads closer to JavaScript, but it still isn't JavaScript: it needs its own compiler transform (Babel, or the TypeScript compiler) before it's real JS at all:

function List({ items }) {
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

Kopular — a real .html file, but compiled by KopScript's own compiler, into the exact same code a hand-written Render() would produce:

// counter.ks
class Counter : Component {
  public state<number> Count;
  constructor() : base() { this.Count = state(0); }
  public void Increment() { this.Count.Value = this.Count.Value + 1; }
  template from "./counter.html";
}
<!-- counter.html -->
<button (click)="Increment()">Count: {{ Count.Value }}</button>

Note there's no Subscribe call anywhere in counter.ks above — a state<T> field referenced directly in the markup (Count.Value) gets re-render wiring automatically. That's specific to templates; the hand-written Counter further down this page (it reaches its count through an injected CounterService, not a field of its own) still calls Subscribe itself, exactly as before — templates are an alternative way to write Render(), not a requirement, and the two mix freely in the same app. This site's own Home page counter demo is this exact templated component, compiled and running for real.

Sharing state across components (dependency injection)

The same task — a shared counter service, injected into a component — in each framework's own idiomatic style:

Kopular — a plain constructor argument, checked by the compiler:

class CounterService {
  public state<number> Count;
  constructor() { this.Count = state(0); }
  public void Increment() { this.Count.Value = this.Count.Value + 1; }
}

class Counter : Component {
  private CounterService Service;
  constructor(CounterService service) : base() {
    this.Service = service;
    this.Service.Count.Subscribe((number v) => this.Update());
  }
  public override VElement Render() {
    VElement button = VElement.Create("button");
    button.TextContent = "Count: " + this.Service.Count.Value;
    button.OnClick = (Event e) => { this.Service.Increment(); };
    return button;
  }
}

CounterService counter = new CounterService();
Counter c = new Counter(counter);

Angular — a decorator-registered, reflection-based injector. Concise when it works, but a missing or mistyped provider is a runtime NullInjectorError, not a compile error:

@Injectable({ providedIn: 'root' })
class CounterService {
  count = signal(0);
  increment() { this.count.update(v => v + 1); }
}

@Component({
  standalone: true,
  selector: 'app-counter',
  template: `<button (click)="counter.increment()">Count: {{ counter.count() }}</button>`,
})
class CounterComponent {
  constructor(public counter: CounterService) {}
}

React has no built-in answer to this at all — sharing state across components not in a direct parent/child relationship means building the mechanism yourself out of Context, or reaching for a separate state-management library:

const CounterContext = createContext(null);

function CounterProvider({ children }) {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  return (
    <CounterContext.Provider value={{ count, increment }}>
      {children}
    </CounterContext.Provider>
  );
}

function Counter() {
  const { count, increment } = useContext(CounterContext);
  return <button onClick={increment}>Count: {count}</button>;
}

For a genuinely simple, self-contained component with no sharing involved, React's hooks are the most concise of the three. Kopular's advantage isn't fewer characters — it's that the wiring above is checked by the compiler in every case, not just the simple one.

Compile-time safety

Bug classKopular / KopScriptAngular (TypeScript)React (JavaScript)
Wrong argument type passed to a functionCompile error, alwaysCompile error, if strict mode is on and any is avoidedRuntime bug — plain JS has no static types at all
Missing/mistyped injected dependencyCompile errorRuntime NullInjectorErrorN/A — no built-in DI to get wrong
Accessing a possibly-null valueCompile error unless narrowed first (T?)Compile error only with strictNullChecks enabledRuntime TypeError
Unhandled case in a multi-way branchCompile error — match requires a _ wildcardNot enforced without a manual exhaustiveness trickNot enforced

TypeScript (Angular's, or React's if adopted) can catch most of the same classes of bug — if strict settings are turned on and any is avoided in practice, neither of which the language itself enforces. KopScript has no equivalent escape hatch and no optional strictness setting: every check above is unconditional.

For the runtime-performance numbers behind these tradeoffs, and for real, measured data on how often an AI actually generates each stack correctly on the first try, see the Benchmarks page.