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.
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.
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.
| Bug class | Kopular / KopScript | Angular (TypeScript) | React (JavaScript) |
|---|---|---|---|
| Wrong argument type passed to a function | Compile error, always | Compile error, if strict mode is on and any is avoided | Runtime bug — plain JS has no static types at all |
| Missing/mistyped injected dependency | Compile error | Runtime NullInjectorError | N/A — no built-in DI to get wrong |
| Accessing a possibly-null value | Compile error unless narrowed first (T?) | Compile error only with strictNullChecks enabled | Runtime TypeError |
| Unhandled case in a multi-way branch | Compile error — match requires a _ wildcard | Not enforced without a manual exhaustiveness trick | Not 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.