Perry compiles a practical subset of TypeScript. This page documents what's not supported or works differently from Node.js/tsc.
Declared TypeScript types are not enforced at runtime — Perry doesn't generate
type guards from annotations, so a parameter typed string will accept a number
without throwing.
{{#include ../../examples/language/limitations.ts:erased-types}}Annotations are mostly erased, with one exception: when emitDecoratorMetadata
applies, the design:type / design:paramtypes reflection metadata is derived
from the annotations on decorated members and survives to runtime (see
Decorators). Runtime type discrimination is available via
explicit typeof checks and instanceof.
Perry compiles to native code ahead of time. Dynamic code execution is not possible:
// Not supported
eval("console.log('hi')");
new Function("return 42");
Perry parses decorator syntax, supports compile-time-only transforms
(see the bundled @log example), and has a reduced legacy TypeScript
compatibility path for class decorators, method decorators, constructor
parameter decorators, method parameter decorators, and property
decorators. That path emits design:paramtypes for decorated
classes/methods, design:type for decorated properties, and implements
Reflect.defineMetadata, Reflect.getMetadata,
Reflect.getOwnMetadata, Reflect.hasMetadata,
Reflect.hasOwnMetadata, Reflect.getMetadataKeys,
Reflect.getOwnMetadataKeys, Reflect.deleteMetadata, and
@Reflect.metadata(...).
Accessor decorators, descriptor replacement, general
Reflect.metadata(...) calls outside decorator syntax, Symbol
metadata keys, and full Angular / NestJS / TypeORM runtime metadata flows
are not supported. See Decorators for details and a
worked migration recipe.
Perry implements a small metadata subset for legacy decorators. General runtime reflection is not supported:
Reflect.getMetadata("design:type", target, key);
Reflect.getMetadataKeys(target, key);
// Not supported as a general helper call outside decorator syntax
Reflect.metadata("design:type", String)(target, key);
Use static ESM imports in Perry source:
// Supported
import { foo } from "./module";
// Not supported
const mod = require("./module");
const mod = await import("./module");
Perry has internal CommonJS compatibility paths for some npm package wrappers,
but user-written modules should use static import declarations.
Perry compiles classes to fixed structures. Dynamic prototype modification is not supported:
// Not supported
MyClass.prototype.newMethod = function() {};
Object.setPrototypeOf(obj, proto);
Object.getPrototypeOf(...) and Reflect.getPrototypeOf(...) are supported
for class/prototype inspection patterns, but Object.setPrototypeOf(...) /
Reflect.setPrototypeOf(...) do not mutate Perry's fixed class layout.
WeakMap, WeakSet, WeakRef, and FinalizationRegistry are implemented and
their APIs behave as expected — set / get / has / delete, add,
deref(), and register / unregister all work and return the right values.
WeakMap and WeakSet use reference equality, so two distinct objects
never collide on the same slot.
The one caveat is that Perry's garbage collector does not yet treat these references as weak, so targets are retained rather than collected. In practice:
WeakRef.deref()always returns the original target (it is never reported as collected).FinalizationRegistryrecords registrations but never fires its cleanup callback.WeakMap/WeakSetkeep their keys alive (they behave like a reference-keyedMap/Set).
This is safe for correctness — code that reads through these APIs gets the right values. It only matters if you depend on collection timing to reclaim memory or to run finalizer side effects.
Proxy support is not a full engine-level trap layer for every possible dynamic object access. Prefer plain objects and explicit APIs unless a package only needs Perry's supported Proxy surface.
Perry supports real multi-threading via parallelMap and spawn from perry/thread. See Multi-Threading.
Threads do not share mutable state — closures passed to thread primitives cannot capture mutable variables (enforced at compile time). Values are deep-copied across thread boundaries. There is no SharedArrayBuffer or Atomics.
Not all npm packages work with Perry:
- Natively supported: ~50 popular packages (fastify, mysql2, redis, etc.) — these are compiled natively. See Standard Library.
compilePackages: Pure TS/JS packages can be compiled natively via configuration.- Not supported: Packages requiring native addons (
.nodefiles),eval(), dynamicrequire(), or Node.js internals.
For cases where you need dynamic behavior, use the JavaScript runtime fallback:
import { jsEval } from "perry/jsruntime";
// Routes specific code through QuickJS for dynamic evaluation
Since there's no runtime type checking, use explicit checks:
{{#include ../../examples/language/limitations.ts:type-narrowing}}- Supported Features — What does work
- Type System — How types are handled