Sharpen

Write TypeScript and React Like C#

Sharpen compiles it to real .tsx/.ts files you keep and own — plain TypeScript, nothing more. Your source lives in .csx files, built for teams who already think in C# and want that fluency on a TypeScript/React/Node stack, starting a brand new project.

  • Not compiled C# — output is plain TypeScript, nothing else runs.
  • Not for existing codebases — new projects only, from day one.
  • Not a runtime or bundler plugin — it just generates TypeScript files; your app never depends on it.
Counter.csx
[State] private int count = 0;
private void Increment() => count++;
↓ compiles to
Counter.tsx
const [count, setCount] = useState<number>(0);
const increment = () => setCount(count + 1);
Practice

How to Run

Sharpen is a build-time generator, not a bundler plugin. It reads a .csx file once and writes a real .tsx/.ts file next to it — nothing downstream ever touches .csx.

Install
npm install -g @mail4hafij/sharpen
Compile a single file
sharpen Foo.csx -o Foo.tsx
Compile a whole project
sharpen --dir ./src
Licensing

License

Sharpen is free to use for 30 days from the first time it runs on a given machine. After that, a license is required to keep using it.

To request a license, email mail4hafij@yahoo.com.

Tooling

Editor Support

A VS Code extension adds syntax highlighting for .csx files.

Install
code --install-extension mail4hafij.sharpen-csx

Or search "Sharpen" in VS Code's Extensions panel. Syntax highlighting only for now — no IntelliSense or go-to-definition yet.

Reference

Documentation

Every real feature, grouped by what it touches: React-specific syntax, then general-purpose TypeScript syntax that applies equally on the backend.

React Syntax

Component — [Component], JSX Render()

Greeting.csx
[Component]
public class Greeting
{
    [Prop] private string name;
    public JSX Render() => <p>Hello, {name}!</p>;
}
Greeting.tsx
interface GreetingProps {
    name: string;
}
export function Greeting({ name }: GreetingProps) {
    return (<p>Hello, {name}!</p>);
}

State & Props — [State], [Prop]

Counter.csx (excerpt)
[State] private int count = 0;
private void Increment() => count++;
Counter.tsx (excerpt)
const [count, setCount] = useState<number>(0);
const increment = () => setCount(count + 1);

Children — [Prop] private JSX children;

[Prop] private JSX children;
public JSX Render() => <div className="panel">{children}</div>;

JSX maps to ReactNode. Lowercase children is already React's own reserved prop name — React itself, not Sharpen, populates it whenever a parent nests content inside <YourComponent>...</YourComponent>.

Conditionals & loops in markup — @if / @foreach

NumberList.csx (excerpt)
@if (numbers.Count == 0)
{
    <p>No numbers yet.</p>
}
else
{
    <ul>
        @foreach (var n in numbers)
        {
            <li>{n}</li>
        }
    </ul>
}
NumberList.tsx (excerpt)
{numbers.length === 0 ? <p>No numbers yet.</p> : <ul>{numbers.map(n => <li>{n}</li>)}</ul>}

Fragments — <>...</>

Works both as a component's whole Render() return value, and inside an @if/@foreach branch — the only way to return more than one sibling element from a branch.

.csx
public JSX Render() =>
    <>
        <p>Seed: {seed}</p>
        <button onClick={Increment}>+1</button>
    </>;
.tsx
return (<><p>Seed: {seed}</p><button onClick={increment}>+1</button></>);

Effect — [Effect], [Effect(Layout = true)]

RenderCounter.csx (excerpt)
[Effect(Deps = nameof(value))]
private void TrackUpdate() => updateCount++;
RenderCounter.tsx (excerpt)
useEffect(() => {
    setUpdateCount(updateCount + 1);
}, [value]);

More than one dependency works too — [Effect(Deps = nameof(a), nameof(b))] compiles to [a, b], not just the first one. [Effect(Layout = true)] compiles to useLayoutEffect instead of useEffect — same body/deps handling either way, coexists with Deps on the same attribute. An async effect body runs inside an IIFE automatically, since useEffect's own callback can't be async.

Refs — [Ref], [ForwardedRef]

AutoFocusInput.csx — a ref you create locally
[Ref] private HTMLInputElement inputRef;

[Effect]
private void FocusOnMount() => inputRef.current?.focus();

public JSX Render() => <input ref={inputRef} />;
ForwardRefInput.csx — a ref received from a parent
[Component]
[Memoized]
public class ForwardRefInput
{
    [Prop] private string placeholder;
    [ForwardedRef] private HTMLInputElement inputRef;

    public JSX Render() => <input placeholder={placeholder} ref={inputRef} />;
}
ForwardRefInput.tsx
export const ForwardRefInput = memo(forwardRef<HTMLInputElement, ForwardRefInputProps>(function ForwardRefInput({ placeholder }: ForwardRefInputProps, inputRef) {
    return (<input placeholder={placeholder} ref={inputRef}/>);
}));

Memoization — [Memo], [Callback], [Memoized]

AttributeApplies toCompiles to
[Memo(Deps = nameof(x))]a method returning a valueuseMemo — calling it in source becomes the bare value, not a call
[Callback(Deps = nameof(x))]a methoduseCallback — stays a real callable in source, unlike [Memo]
[Memoized]a [Component] class, stacked next to [Component]wraps the whole component in React's memo()
ListSummary.csx
[Memo(Deps = nameof(numbers))]
private int Total() => numbers.Sum();

public JSX Render() => <p>Total: {Total()}</p>;
ListSummary.tsx
const total = useMemo(() => numbers.reduce((a, b) => a + b, 0), [numbers]);
return (<p>Total: {total}</p>);

Deps supports more than one dependency here too, the same way [Effect] does — Deps = nameof(a), nameof(b) keeps both, not just the first.

Context & Provider — [Context], [UseContext], dotted JSX tags

ThemeContext.csx
[Context]
public class ThemeContext
{
    public string Theme { get; set; } = "light";
}
Consuming it
[UseContext] private ThemeContext theme;
public JSX Render() => <button className={theme.Theme}>Click me</button>;
Providing it — dotted JSX tag names work, e.g. Context.Provider
public record ThemeValue(string Theme);

[Prop] private JSX children;

public JSX Render() =>
    <ThemeContext.Provider value={new ThemeValue("dark")}>
        {children}
    </ThemeContext.Provider>;

A dotted tag's root gets the same automatic cross-file import a [UseContext] field already gets — no explicit import needed even when the context lives in a different file.

Store — [Store], [Observable], [Inject]

CounterStore.csx
[Store]
public class CounterStore
{
    [Observable] public int Count { get; set; } = 0;
    public void Increment() => Count++;
}
CounterStore.ts
export const useCounterStore = create<{
    count: number;
    increment: () => void;
}>(set => ({
    count: 0,
    increment: () => set(s => ({ count: s.count + 1 })),
}));

Bare [Inject] subscribes to the whole store (every mutation re-renders). [Inject(Select = nameof(Store.Member))] subscribes to just one field (useStore(s => s.member)) — use this for anything performance-sensitive. A store action can be a real async API call — full statement grammar (locals, if/try, await), not just a bare mutation. See the Restaurant App below for a full example.

Custom hooks — [Hook]

useCounter.csx
public record CounterHook(int Count, Action Increment, Action Reset);

[Hook]
public static CounterHook UseCounter(int initial = 0)
{
    [State] int count = initial;
    void increment() => count++;
    void reset() => count = initial;
    return new CounterHook(count, increment, reset);
}
useCounter.ts
export function useCounter(initial: number = 0): CounterHook {
    const [count, setCount] = useState<number>(initial);
    const increment = () => setCount(count + 1);
    const reset = () => setCount(initial);
    return { count, increment, reset };
}

TypeScript Syntax

No React here — plain classes and functions, equally usable in the Node.js backend.

Types

Every C# type this compiler understands, and what it becomes.

C#TypeScript
int, double, float, longnumber
stringstring
boolboolean
voidvoid
T?T | null
List<T>T[]
Action() => void
Action<T1, T2, ...>(arg0: T1, arg1: T2, ...) => void
TaskPromise<void>
Task<T>Promise<T>
dynamicany
JSXReactNode

dynamic is for a shape deliberately left unmodeled — e.g. a raw database row from a driver, with no ORM in front of it. JSX is used for a [Prop] meant to hold renderable content, most commonly a component's children.

Imports — using

using { readFileSync } from "fs";
using path from "path";
using * as cors from "cors";
import { readFileSync } from "fs";
import path from "path";
import * as cors from "cors";

A relative-path using (e.g. using { Foo } from "./Foo";) is recognized as your own code, so ClassName.Method() correctly camelCases the member even called by class name. A real package import keeps its casing untouched.

Classes & functions

Rectangle.csx
public class Rectangle
{
    public double Width = 0;
    public double Height = 0;
    public double Area() => Width * Height;
}
Rectangle.ts
export class Rectangle {
    public width: number = 0;
    public height: number = 0;
    public area(): number {
        return this.width * this.height;
    }
}

A plain function works the same way: no attribute needed, just a real exported function with real parameters, arithmetic, and local variables.

Records & enums

.csx
public record Todo(int Id, string Text, bool Done);
private enum Status { Loading, Success, Error }
.ts / .tsx
export interface Todo {
    id: number;
    text: string;
    done: boolean;
}
type Status = "loading" | "success" | "error";

new Todo(1, "Buy milk", false) compiles to a plain object literal { id: 1, text: "Buy milk", done: false }; todo with { Done = true } compiles to { ...todo, done: true }. Both a record and an enum work equally well declared top-level (as here) or nested inside a class/component/store/context.

Control flow — if, for, foreach, ternary

.csx
int total = 0;
for (int i = 0; i < n; i++)
{
    total = total + i;
}
return total > 10 ? "big" : "small";
.ts
let total: number = 0;
for (let i: number = 0; i < n; i++) {
    total = total + i;
}
return total > 10 ? "big" : "small";
.csx — foreach
foreach (var word in words)
{
    console.log(word);
}
.ts
for (const word of words) {
    console.log(word);
}

foreach compiles to a real for...of, not .forEach(). while/do and a plain switch aren't supported yet — deferred until something concretely needs them.

Operators

C#TypeScript
+ - * /identical
< > <= >=identical
== !==== !== (strict, not loose)
&& ||identical
??identical
cond ? a : bidentical

?? sits outermost of the three (loosest), then ||, then && — a simplification of real JS's stricter mixing rules, never produces a wrong-precedence result.

Lambdas

.csx
var doubled = numbers.Select(n => n * 2);
var total = doubled.Aggregate(0, (a, b) => a + b);
.ts
let doubled = numbers.map(n => n * 2);
let total = doubled.reduce((a, b) => a + b, 0);

A single bare parameter needs no parentheses (n => ...); more than one does ((a, b) => ...). Expression-bodied only — a block-bodied lambda (() => { ... }) isn't supported yet.

Optional chaining & null-forgiving

inputRef.current?.focus();
someValue!.doSomething();

Template literals — $"..."

.csx
var greeting = $"Hello, {name}! You have {count} item{(count == 1 ? "" : "s")}.";
.ts
let greeting = `Hello, ${name}! You have ${count} item${count === 1 ? "" : "s"}.`;

C#'s own interpolated-string syntax, not JS's backtick literal directly — stays consistent with "write real C#, get idiomatic TS out." Each {...} hole is parsed as a fully independent expression (arbitrary nesting works).

Arrays — spread, indexing, as

Categories = [.. Categories, created];   // -> [...categories, created]
var rows = result[0];                     // tuple/array indexing
var info = result[0] as ResultSetHeader;  // type assertion, compile-time only

LINQ (LINQ-to-Objects only)

Every method below operates on a plain in-memory array — the same as Array.prototype itself. There is no LINQ-to-SQL translation anywhere; the backend talks to the database via raw SQL strings regardless.

LINQCompiles to
Where(pred).filter(pred)
Select(fn).map(fn)
ToList()(no-op — already an array)
Sum().reduce((a,b)=>a+b,0)
OrderBy(key).slice().sort(...) — non-mutating
Any() / Any(pred).length > 0 / .some(pred)
All(pred).every(pred)
First() / FirstOrDefault()[0] / .find(pred)
Count() / Count(pred).length / .filter(pred).length
Skip(n) / Take(n).slice(n) / .slice(0, n)
Reverse().slice().reverse() — non-mutating
Distinct()[...new Set(arr)] (reference equality for objects)
Min() / Max()Math.min(...arr) / Math.max(...arr)
Aggregate(seed, fn).reduce(fn, seed)
Join(inner, ok, ik, result)a nested .flatMap()/.filter()/.map() — one row per matching pair, like a SQL inner join

GroupBy/ToDictionary/ToLookup aren't implemented — their result is a different shape entirely (a grouped sequence, a Map), not just another array.

Async, await, try/catch

.csx
[Effect(Deps = nameof(userId))]
private async void LoadUser()
{
    try
    {
        await fetchUser(userId);
        status = Status.Success;
    }
    catch
    {
        status = Status.Error;
    }
}

A method can also return a real value asynchronously — Task/Task<T> is the real async return type (see Types above), not void:

.csx
public class Repo
{
    public static async Task<int> CountRows(string sql)
    {
        var result = await Query(sql);
        return result;
    }
}
.ts
export class Repo {
    public static async countRows(sql: string): Promise<number> {
        let result = await Query(sql);
        return result;
    }
}

Error Handling

Every error csx reports is exactly one of three kinds — the kind itself tells you what to do next. Every error includes the exact file, line, and column, plus a short code frame pointing right at the problem.

KindMeaningWhat to do
Syntax errorYou wrote something invalid — malformed grammar, or a real rule violation like a store action that never mutates stateFix your .csx
Unsupported featureValid C#, but this compiler doesn't emit it yetNot a mistake — restructure around the known gap
Internal compiler errorThe compiler itself hit something it didn't expectNot your fault — please report it

Syntax errors — malformed grammar

Something the parser can't make sense of at all — a missing token, a bad tag, an unterminated string.

Greeting.csx (missing semicolon)
[Component]
public class Greeting
{
    [Prop] private string name
    public JSX Render() => <p>Hello, {name}!</p>;
}
Terminal output
Greeting.csx:5:5 - syntax error: Expected ";"
5 |     public JSX Render() => <p>Hello, {name}!</p>;
        ^
Greeting.csx (mismatched JSX closing tag)
[Component]
public class Greeting
{
    [Prop] private string name;
    public JSX Render() => <p>Hello, {name}!</div>;
}
Terminal output
Greeting.csx:5:50 - syntax error: Mismatched JSX closing tag: expected </p>, got </div>
5 |     public JSX Render() => <p>Hello, {name}!</div>;
                                                     ^

Semantic rule violations

Grammatically fine, but breaks a real rule the compiler enforces — reported as the same "syntax error" kind, since either way the fix is in your .csx. The one rule checked today: an expression-bodied [Store] action must actually mutate an [Observable] property.

CounterStore.csx (an action that reads state but never changes it)
[Store]
public class CounterStore
{
    [Observable] public int Count { get; set; } = 0;
    public int Peek() => Count;
}
Terminal output
CounterStore.csx:5:5 - syntax error: Store method "Peek" must mutate an [Observable] property
5 |     public int Peek() => Count;
        ^

Unsupported features

Real, valid C# — just not emitted yet. Not a mistake to fix, a gap to restructure around.

Greeting.csx (Render must be expression-bodied — no block body yet)
[Component]
public class Greeting
{
    [Prop] private string name;
    public JSX Render() { return <p>Hello, {name}!</p>; }
}
Terminal output
Greeting.csx:5:25 - unsupported feature: Render() must be expression-bodied (=> JSX); block bodies are not supported for Render yet
5 |     public JSX Render() { return <p>Hello, {name}!</p>; }
                            ^

Multiple errors at once

A mistake in one declaration doesn't hide a mistake in another — every independent problem in a file (and every broken file in a --dir run) is collected and reported together in one pass, not discovered one fix-and-recompile cycle at a time.

Dashboard.csx (an unsupported block-bodied [Memo] AND a store rule violation, in the same file)
[Component]
public class Dashboard
{
    [Prop] private string name;

    [Memo(Deps = nameof(name))]
    private string Shout()
    {
        return name;
    }

    public JSX Render() => <p>{name}</p>;
}

[Store]
public class CounterStore
{
    [Observable] public int Count { get; set; } = 0;
    public int Peek() => Count;
}
Terminal output — both reported, in one run
Dashboard.csx:6:5 - unsupported feature: Block-bodied [Memo] method "Shout" isn't supported yet
6 |     [Memo(Deps = nameof(name))]
        ^
Dashboard.csx:19:5 - syntax error: Store method "Peek" must mutate an [Observable] property
19 |     public int Peek() => Count;
         ^

Internal compiler errors

Looks the same way — file:line:col plus a code frame when a position is available — but means the mistake is on the compiler's side, not yours. There's nothing to fix in your .csx for one of these; it exists so a raw crash never leaks out disguised as your own bug, and it's always worth reporting.

Complete Example

The Restaurant App

A small real app — categories and menu items — with both halves built and verified end to end: a React frontend calling a real Node.js/Express/MySQL API, both written entirely in .csx and compiled by Sharpen.

The architecture, briefly:

  • One [Store] holds both the domain data (categories/items) and shared UI state (which modal is open, draft field values) — that's what lets List/Add/Edit live in separate files without passing a full object across them as a prop.
  • Each List owns its own Add/Edit as children, not as siblings — delete is handled by List itself.
  • Every component selects only the store slices it actually reads ([Inject(Select = ...)]), not the whole store.
  • Store actions are real async API calls — await fetch(...), real response handling, adopting the server's response instead of inventing state locally.

Frontend

File structure
app/src/
├── App.csx
├── stores/
│   └── RestaurantStore.csx   categories, items, and all shared UI state
└── pages/
    ├── Category/
    │   ├── CategoryList.csx  renders CategoryAdd + CategoryEdit as children
    │   ├── CategoryAdd.csx
    │   └── CategoryEdit.csx
    └── Item/
        ├── ItemList.csx      renders ItemAdd + ItemEdit as children
        ├── ItemAdd.csx
        └── ItemEdit.csx
RestaurantStore.csx — a real async store action
public async Task SaveNewCategory()
{
    var body = JSON.stringify(new NewCategoryBody(CategoryDraftName, "/images/category-placeholder.svg"));
    var response = await fetch(ApiUrl("/categories"), JsonRequest("POST", body));
    var created = await response.json();
    Categories = [.. Categories, created];
    SelectedCategoryId = created.Id;
    ShowAddCategoryForm = false;
}
CategoryList.csx — narrow selects, renders its own Add/Edit
[Component]
public class CategoryList
{
    [Inject(Select = nameof(RestaurantStore.Categories))] private List<Category> categories;
    [Inject(Select = nameof(RestaurantStore.SelectedCategoryId))] private int selectedCategoryId;
    [Inject(Select = nameof(RestaurantStore.SelectCategory))] private Action<int> selectCategory;
    [Inject(Select = nameof(RestaurantStore.DeleteCategory))] private Action<int> deleteCategory;

    public JSX Render() =>
        <div>
            <div className="category-grid">
                @foreach (var c in categories)
                {
                    <div key={c.Id} className={c.Id == selectedCategoryId ? "category-card selected" : "category-card"}>
                        <p onClick={() => selectCategory(c.Id)}>{c.Name}</p>
                        <button onClick={() => deleteCategory(c.Id)}>Delete</button>
                    </div>
                }
            </div>
            <CategoryAdd />
            <CategoryEdit />
        </div>;
}
CategoryAdd.csx — a modal driven entirely by shared store state
[Component]
public class CategoryAdd
{
    [Inject(Select = nameof(RestaurantStore.ShowAddCategoryForm))] private bool showAddCategoryForm;
    [Inject(Select = nameof(RestaurantStore.CategoryDraftName))] private string categoryDraftName;
    [Inject(Select = nameof(RestaurantStore.SetCategoryDraftName))] private Action<string> setCategoryDraftName;
    [Inject(Select = nameof(RestaurantStore.SaveNewCategory))] private Action saveNewCategory;

    public JSX Render() =>
        <div>
            @if (showAddCategoryForm)
            {
                <div className="modal">
                    <input value={categoryDraftName} onChange={e => setCategoryDraftName(e.target.value)} />
                    <button onClick={saveNewCategory}>Save</button>
                </div>
            }
        </div>;
}

Backend

A real Node.js/Express backend, MySQL-backed (schema in api/schema.sql). 8 endpoints, verified with real HTTP requests against the real database.

ResourceListAddEditDelete
Category GET /categories POST /categories PUT /categories/:id DELETE /categories/:id
Item GET /items POST /items PUT /items/:id DELETE /items/:id
File structure
api/src/
├── main.csx                    entry point - see the Main() convention below
├── db/
│   └── Database.csx            static factory: Database.Connect() -> Pool
├── categories/
│   ├── Category.csx            record Category(Id, Name, ImageUrl)
│   └── CategoryRepository.csx  static methods: List/Add/Edit/Delete against MySQL
├── items/
│   ├── MenuItem.csx
│   └── ItemRepository.csx
└── routes/
    ├── CategoryRoutes.csx      static methods: registers the 4 category endpoints
    └── ItemRoutes.csx          static methods: registers the 4 item endpoints
CategoryRepository.csx — a real MySQL query
public static async Task<int> Edit(Pool pool, int id, string name, string imageUrl)
{
    var result = await pool.query("UPDATE categories SET name = ?, image_url = ? WHERE id = ?", [name, imageUrl, id]);
    var info = result[0] as ResultSetHeader;
    return info.affectedRows;
}
routes/CategoryRoutes.csx — registering endpoints, checking for a real row
public static void Register(Express app, Pool pool)
{
    app.get("/categories", (req, res) => List(pool, req, res));
    app.put("/categories/:id", (req, res) => Edit(pool, req, res));
}

public static async Task Edit(Pool pool, Request req, Response res)
{
    var id = Number(req.params.Id);
    var affected = await CategoryRepository.Edit(pool, id, req.body.Name, req.body.ImageUrl);
    if (affected == 0)
    {
        res.status(404).end();
        return;
    }
    res.json(new Category(id, req.body.Name, req.body.ImageUrl));
}
main.csx — the entry-point convention
using { Database } from "./db/Database";
using { CategoryRoutes } from "./routes/CategoryRoutes";
using { ItemRoutes } from "./routes/ItemRoutes";

public async void Main()
{
    var pool = Database.Connect();
    Express app = express();
    app.use(express.json());
    CategoryRoutes.Register(app, pool);
    ItemRoutes.Register(app, pool);
    app.listen(4000, () => console.log("API listening on http://localhost:4000"));
}

A plain function literally named Main is auto-invoked at the bottom of the file — the one way to get a top-level statement in a grammar where everything else is a declaration. No constructors exist yet, so every backend class here is a static-method namespace, not an instance.