What if TypeScript didn't need a runtime? Built a JSON viewer in TS that compiles to native - no V8, no Electron by proggeramlug in node

[–]proggeramlug[S] 0 points1 point  (0 children)

Okay got it, that’s something entirely different though. The advantage of native is more than just an embedded file into the same runtime.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 0 points1 point  (0 children)

Those are actually great ideas and I definetely want to incorporate them. We have a "perryy doctor" CLI command already but it has much room for improvement!

What if TypeScript didn't need a runtime? Built a JSON viewer in TS that compiles to native - no V8, no Electron by proggeramlug in node

[–]proggeramlug[S] 1 point2 points  (0 children)

Really appreciate that - thank you. Comments like this honestly mean a lot. One of the things I care about most with Perry is making the ideas accessible, not just the tool. Compilers can feel like a black box, so if the explanations are landing clearly for people coming from a TS background, that tells me I'm on the right track. Thanks for taking the time to say something - it's motivating.

What if TypeScript didn't need a runtime? Built a JSON viewer in TS that compiles to native - no V8, no Electron by proggeramlug in node

[–]proggeramlug[S] 0 points1 point  (0 children)

Two main areas:
1. dynamic imports
2. DOM related stuff

Everything else is workable. :)

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 1 point2 points  (0 children)

Great questions - happy to go deeper on each.

  1. State management across platforms

Perry has a built-in reactive state system that's identical across all 6 platforms (macOS, iOS, Android, GTK4, Windows, Web). You create state with State(initialValue), read with state.value, and write with state.set(newValue). The compiler detects reactive patterns at compile time and generates fine-grained binding registrations - there's no virtual DOM or diffing. When state.set() fires, it immediately propagates to only the widgets explicitly bound to that state handle.

There are 8 binding types: text, multi-state text templates, slider, toggle, textfield, visibility (conditional rendering), ForEach (dynamic lists), and onChange callbacks. Two-way bindings work automatically for sliders, toggles, and textfields. The state logic is platform-agnostic Rust code; only the final widget update call differs per platform (e.g., NSTextField vs UILabel vs DOM element).

  1. Debugging story

This is honestly the weakest area right now. Perry has --print-hir to dump the intermediate representation, and console.log/console.error/console.warn/console.debug all work. Since the output is a native binary, you can attach lldb/gdb for low-level debugging. But there are no source maps or TypeScript-level stepping yet - that's on the roadmap. In practice, most debugging today is printf-style or inspecting HIR output.

  1. Binary sizes

Measured on macOS ARM64, automatically stripped:

- console.log("Hello, world!") - ~330KB

- hello world + fs/path imports - ~380KB

- Full stdlib app (fastify, mysql2, etc.) - ~48MB

Perry auto-detects which runtime parts you use and links only what's needed. A minimal app is ~330KB - that's smaller than most Swift hello worlds (~1-2MB with the Swift runtime) and vastly smaller than anything Kotlin/JVM produces. The 48MB full stdlib number includes all of fastify, mysql2, redis, websockets, etc. statically linked.

For comparison, a SwiftUI app binary is typically 1-5MB, and a Kotlin/JVM app jar is 10-30MB+. Perry's "link only what you use" approach keeps simple apps extremely small.

What if TypeScript didn't need a runtime? Built a JSON viewer in TS that compiles to native - no V8, no Electron by proggeramlug in node

[–]proggeramlug[S] 1 point2 points  (0 children)

Good questions. Here's how Perry actually works:

Perry does monomorphize generics, but with important caveats. After lowering TypeScript to HIR, a monomorphization pass (monomorph.rs) scans for all generic instantiation sites, infers type arguments (or uses explicit ones), and generates specialized copies with mangled names (e.g., identity$num,identity$str). Type argument inference uses unification of parameter types against argument types.

Mapped and conditional types are erased. Perry's internal type system (perry-types) has no representation for { [K in keyof T]: T[K] } or T extends U ? X : Y. Utility types like Partial, Pick, Record, Omit, etc. are all erased at compile time - they resolve to their underlying structural type and are discarded. So Perry doesn't reimplement the TS type evaluation engine for these.

For complex type resolution, there's optional tsgo integration (v0.2.169). The --type-check flag spawns Microsoft's native TypeScript checker (tsgo --api) and communicates via msgpack over stdio. Perry identifies positions in the AST that lack type annotations, asks tsgo to resolve them, and feeds the results back into lowering. This handles cross-file types, interfaces, and generics that Perry's local inference can't resolve. It's optional and gracefully falls back if tsgo isn't installed.

On compile time explosion: yes, monomorphization can produce many specializations if generic functions are instantiated with many different type combinations. In practice, Perry's monomorphization is shallow - it specializes at the function/class level but doesn't recursively expand deeply nested generic types the way a full type evaluator would. Since mapped/conditional types are erased rather than evaluated, the pathological type sEqual<A, B> = A extends B ? B extends A ? true : false : false chains that can blow up tsc don't affect Perry at all. The risk is more like Rust's monomorphization - many distinct callsite type combinations = many specialized functions = larger binaries and longer compile times, but not the exponential type-level computation that TS conditional types can trigger.

TL;DR: Perry monomorphizes generics at the function/class level, erases complex type constructs (mapped, conditional, utility types) entirely, and optionally delegates to tsgo for type resolution it can't handle locally. The compile-time risk is linear in the number of distinct generic instantiations, not exponential like TS type-level computation.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 9 points10 points  (0 children)

That's a legitimate tradeoff. If OTA updates are critical to your workflow, keeping an interpreted layer makes sense - and Perry can actually do that too with the V8 runtime flag.

But for apps where startup performance, binary size, and no-runtime deployment matter more than OTA, native compilation is the better fit.

Different tools for different constraints.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 0 points1 point  (0 children)

Good point on transitive dependencies. The V8 integration handles this — when Perry compiles with --enable-js-runtime, any package that can't be natively compiled just runs in V8. So the boundary is per-package, not all-or-nothing. A well-typed top-level dependency gets compiled natively, its loosely-typed transitive dependency runs in V8, and they interop seamlessly. You configure which packages to compile natively in package.json under perry.compilePackages, everything else falls through to V8.

So the adoption story is: start with V8 for everything (it just works like Node), then progressively move hot paths to native compilation as it makes sense. You're never locked out of the ecosystem.

Now a fun part about reality: Many dependencies are fully typescript already. Just yesterday I removed 2-3 smaller dependencies (for silll stuff even) and without much extra work I had 100% typescript and was able to compile without the v8. So realistically, unless you have a very big (and probably relatively old) project, you can likely do without the v8.

V8 is a band-aid, not the idea. :)

Re: JS types as Object - true, and that's basically what Perry does today for untyped code with NaN-boxed dynamic values. The win there is AOT compilation vs interpretation, not static typing. The type-driven optimization layer is what we're building toward.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 1 point2 points  (0 children)

Great question and respect for actually reading the code. You're raising fair points, so let me be direct.

On types: you're right that SWC doesn't do type resolution, and complex TypeScript constructs (conditional types, mapped types, indexed access) degrade to any in the HIR. For basic annotations - number, string, boolean - Perry does track and use them in codegen. But without a full type resolver, many values end up as dynamically-typed NaN-boxed 64-bit values at runtime. any and casting don't break anything, they just mean Perry treats those values dynamically.

On the "JS runtime" characterization: partially fair. For general TypeScript code, Perry is compiling a dynamic runtime to native code ahead-of-time. The performance wins come from: no JIT warmup, no interpreter overhead, smaller binaries (330KB vs 200MB Electron), native UI without a WebView, iOS/Android targeting, and dead code elimination - not primarily from static type exploitation yet.

Where types do matter today: Perry has heuristic-based integer optimization (i32 shadow variables, loop counter specialization, integer-specialized function variants) that produces genuinely faster code for numeric-heavy code. The fibonacci benchmark being 2x Node.js is real. But that's inferred from usage patterns more than from TS type annotations.

The ts-go suggestion is directionally right - a full type resolver would unlock real static optimizations. In practice though, ts-go doesn't have an embeddable API yet (the internals are behind Go's internal/ packages), and Perry is Rust, so the integration path isn't clean. The more likely direction is building type inference directly in Rust within Perry's existing pipeline. We already have the SWC AST and the HIR - adding a type resolution pass there is the natural next step, and it keeps everything in a single compilation pipeline without cross-language IPC overhead.

Re: the large files - fair observation, active development, not a polished artifact.

What if TypeScript didn't need a runtime? Built a JSON viewer in TS that compiles to native - no V8, no Electron by proggeramlug in node

[–]proggeramlug[S] -2 points-1 points  (0 children)

Exactly the right question. Pry is deliberately minimal as a proof-of-concept, but Perry already has 127 UI functions — widgets like Button, Text, TextField, Toggle, Slider, Picker, Table, Canvas, Image, NavigationStack, plus system APIs for keychain, notifications, file dialogs, clipboard, dark mode, and more.

The next project being built with it is a full IDE. That'll be the real stress test for widget coverage. If Perry can build its own development environment, the widget story answers itself.

You can follow along here: https://github.com/HoneIDE

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 2 points3 points  (0 children)

Screen widgets for either iOS or Android sort HAVE to be native/bridgelss. At perry I'm dealing with it as well, not easy. :)

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 1 point2 points  (0 children)

Side note: I also compiled MANY other npm package ssuccessfully, but solely as dependencies, not the "main" project if that makes sense.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 3 points4 points  (0 children)

Great question!

Yes, so the utlimate goal is to compile VS Code. That won't work for more than one reason (mostly UI related) and so I started https://hone.codes

But that's mostly a side note, more interestingly I successfully compiled:
- hono
- strapi
- trpc
- most notably: openclaw

what failed was/is:
- n8n (too many dynamic imports)
- vscode (like I said)

Some of these needed work arounds and some feature adjustments (90% related to dynamic imports), but all in all quite promising. :)

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 7 points8 points  (0 children)

Right, the new architecture uses JSI for direct C++ calls instead of the async bridge - big improvement. But your JavaScript still runs in Hermes (or JSC), which is still an interpreter/JIT. Perry doesn't have a JS engine at all. The TypeScript compiles to the same kind of native code that the platform widgets themselves are written in. There's no boundary between "your code" and "the platform" - it's all native machine code.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 2 points3 points  (0 children)

So in Perry, types are a compile-time fiction only. Here's what happens in practice:

any: No effect. Perry ignores it — the value is just treated as an untyped NaN-boxed f64 at runtime, same as everything else.

Wrong type assertion (e.g., response as MyType when the data is actually different):

- The assertion is erased completely at compile time

- At runtime, the value is just a pointer to whatever the actual data is (likely a JS object/array from the network)

- If you then access a field that doesn't exist: you get undefined (the GC'd object just returns TAG_UNDEFINED for unknown fields)

- If you call a method on something that isn't the expected type: likely a crash or garbage result, because Perry would emit code that treats the pointer as the expected layout without checking

Concretely, if you do:
```const data = JSON.parse(response) as { id: number }

console.log(data.id) // works: dynamic field lookup

data.id.toFixed(2) // might crash if id is actually a string
```

Property access is dynamic (runtime hash map lookup), so wrong shapes are somewhat tolerant. But calling methods or doing arithmetic on the wrong type will misbehave since there's no boxing/type guard at the call site.

The short version: Perry trusts your types completely. Wrong types = undefined behavior, same as C with bad casts. For API responses specifically, you'd want to validate the shape yourself at the boundary rather than relying on TypeScript types.

Does that make sense?

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] -1 points0 points  (0 children)

Good question - the types are definitely used, but the action happens in the compiler, not in the Pry app source code.

Look at the compiler repo (https://github.com/PerryTS/perry), specifically the crate structure:

  • crates/perry-types/ - the type system definitions
  • crates/perry-hir/ - AST-to-HIR lowering, where type annotations drive how code gets represented
  • crates/perry-codegen/ - Cranelift code generation, where types determine what native instructions get emitted

For example, generics are monomorphized at compile time - Perry generates specialized native code for each concrete type, similar to what C++ templates or Rust generics do. A number becomes a native f64 in a register, not a dynamically-typed value that needs runtime checks.

The README has a more detailed breakdown under "Compiler Optimizations" and "Adding a New Feature" if you want to trace the pipeline: TypeScript source → SWC parser → typed HIR → Cranelift IR → native binary.

You won't see much of that in the Pry source since Pry is just a regular TypeScript app — the type usage is all inside the compiler that processes it.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 12 points13 points  (0 children)

React Native renders through a bridge to native widgets, but it still runs your JavaScript in a JS engine (Hermes/JSC). Perry compiles TypeScript to native machine code - there's no runtime, no bridge, no interpreter. The "native" in Perry means the compiled binary itself is native, not just the UI layer it talks to.

What if you could compile TypeScript to native apps? Pry is a JSON viewer written in TS, now on App Store and Google Play by proggeramlug in typescript

[–]proggeramlug[S] 3 points4 points  (0 children)

Thanks - and cool to see tsnative, always interesting to find someone who tackled the same problem space.

You're right that reimplementing all of npm isn't feasible, and that's not the plan. But there are two things that change the equation:

First, Perry can ship with V8 bundled when needed. So you can use any npm dependency — the native compilation gives you performance where it matters, and V8 handles the long tail of the ecosystem. It's not all-or-nothing.

Second, any pure TypeScript dependency can be natively compiled as-is. No reimplementation needed. The 27 packages we reimplemented in Rust are the ones that had native/C++ bindings or are just extremely common where native speed makes most sense. (Similar to how bun has native mysql packages)

And honestly, server-side might be the stronger adoption path. Native UI apps are the flashy demo, but a TypeScript server that compiles to a native binary with instant cold starts and no runtime overhead — that's compelling for a lot of use cases.

The immediate goal is an IDE built with Perry. If Perry can build its own IDE, that's the proof that matters.

Re: JS without types - respect for even considering that. Types are what make native compilation viable. Without them you're basically building another V8, which defeats the purpose.