TypeScript Extension
The TypeScript extension provides two things:
- Runtime support for
fn typescript ...blocks (V8 via deno_core). - Child-LSP configuration so Shape LSP can delegate hover/completion/diagnostics inside TypeScript bodies.
The extension transpiles TypeScript to JavaScript and executes it in an
embedded V8 isolate (the same engine that powers Node.js and Deno). Each
fn typescript block becomes a wrapper function registered in V8 and invoked
once per call.
Install
Section titled “Install”shape ext install typescriptThis downloads, compiles, and installs the extension to ~/.shape/extensions/.
Both the CLI and LSP will automatically detect it — no further configuration
needed.
Build from source (alternative)
Section titled “Build from source (alternative)”If you need a development build from the workspace:
cargo build -p shape-ext-typescript --releaseThis produces target/release/libshape_ext_typescript.so (Linux).
Load the Extension at Runtime
Section titled “Load the Extension at Runtime”If you installed via shape ext install, the extension is loaded automatically.
For manual loading, use any of the following.
1. CLI shorthand
Section titled “1. CLI shorthand”shape run script.shape --extension typescript2. Script frontmatter
Section titled “2. Script frontmatter”---[[extensions]]name = "typescript"---3. Project shape.toml
Section titled “3. Project shape.toml”[[extensions]]name = "typescript"autoload = trueExtensions installed via shape ext install are resolved by name from
~/.shape/extensions/. You can also specify an explicit path for
development builds.
Writing TypeScript Foreign Functions
Section titled “Writing TypeScript Foreign Functions”Declare a TypeScript function with fn typescript:
fn typescript add(a: int, b: int) -> Result<int> { return a + b}
let result = add(1, 2)// result: Ok(3)Arguments are marshalled from Shape to V8, the TypeScript body is transpiled to JavaScript and executed, and the return value is marshalled back. Both directions are type-checked against the declared signature.
Return Type Annotation
Section titled “Return Type Annotation”Every fn typescript block must have an explicit return type, and that
return type must be Result<T> — TypeScript is a dynamic-error-model
runtime, so the compiler refuses bare return types to guarantee every call is
forced to handle the failure case.
// Dynamic errors — JS/TS exceptions become Err(...)fn typescript safe_divide(a: number, b: number) -> Result<number> { if (b === 0) throw new Error("division by zero"); return a / b}(Earlier drafts of this chapter showed a “static error model” form without
Result<T>. The compiler now rejects that form with
Foreign function ...: return type must be Result<T> ....)
The runtime advertises error_model: Dynamic in its LanguageRuntimeVTable,
which is why Result<T> is automatically wired up for thrown exceptions.
Type Marshalling Rules
Section titled “Type Marshalling Rules”Values crossing the TypeScript/Shape boundary are converted via MessagePack:
| Shape type | TypeScript type | Direction | Notes |
|---|---|---|---|
int | number | both | Integer values fitting in i32 are passed as V8 Integer; larger values via Number (f64) |
number / float | number | both | IEEE-754 double precision |
string | string | both | UTF-8 |
bool | boolean | both | |
none | null | both | null and undefined both marshal to none |
Vec<T> | Array<T> | both | Each element validated recursively against T |
{f: T, ...} | object | both | Declared fields validated; produces a TypedObject on return |
Coercion rules:
- TypeScript numbers that are exact integers and fit in i64 marshal back to
int; otherwise they marshal back tonumber. booleanandnumberare distinct types — no implicit coercion in either direction.- Type mismatches surface as
Err(MARSHAL_ERROR)in the dynamic error model.
Object Return Types
Section titled “Object Return Types”When a foreign function declares an inline object return type, Shape creates a schema at compile time and validates the JavaScript object on return:
fn typescript fetch_user(id: int) -> Result<{name: string, age: int}> { return { name: "Alice", age: 30 }}
let user = fetch_user(1).unwrap()print(user) // {name: "Alice", age: 30}print(user.name) // "Alice"print(user.age) // 30The returned object must contain all declared fields with the correct types.
Missing fields or type mismatches produce a MARSHAL_ERROR.
Error Model
Section titled “Error Model”With Result<T> return types, two error codes distinguish failure sources:
| Error code | Meaning |
|---|---|
RUNTIME_ERROR | TypeScript/JS threw an exception (throw new Error(...), runtime type error, etc.) |
MARSHAL_ERROR | Return value didn’t match declared type T, or argument deserialization failed |
fn typescript bad_return(x: int) -> Result<int> { return "not an int"}
let r = bad_return(1)// r: Err({code: "MARSHAL_ERROR", ...})Thrown exceptions retain the V8 .stack trace when available; the extension
attempts .stack first, falls back to .message, and last-resort stringifies
the thrown value.
Async Functions
Section titled “Async Functions”Add the async keyword for I/O-bound TypeScript:
async fn typescript fetch_json(url: string) -> Result<{value: number}> { const response = await fetch(url); return await response.json();}async fn typescript wraps the body in an async function, returning a
Promise. The extension uses a cached Tokio runtime (built once per
TsRuntime instance on the first async call) to drive deno_core’s event
loop until the promise resolves. The Shape VM blocks the calling task until
the promise settles.
Enable It in LSP
Section titled “Enable It in LSP”Shape LSP can discover extension declarations from script frontmatter and
shape.toml.
If you want TypeScript always available in the editor (even before the current file declares it), configure always-load extensions in LSP initialization/settings — the keys are the same as for the Python extension (see Python Extension :: Supported LSP Config Keys).
The TypeScript extension advertises typescript-language-server --stdio for
child LSP startup. Make sure typescript-language-server is on your PATH
if you want completions and diagnostics inside TypeScript bodies.
Neovim Example
Section titled “Neovim Example”After installing the extension (shape ext install typescript), the LSP
auto-detects it from ~/.shape/extensions/. No manual path configuration is
needed:
return { { "neovim/nvim-lspconfig", opts = function() vim.filetype.add({ extension = { shape = "shape" } })
vim.lsp.config.shape = { cmd = { "shape-lsp" }, filetypes = { "shape" }, root_markers = { ".git", "shape.toml" }, }
vim.lsp.enable("shape") end, },}Bundled typescript Namespace
Section titled “Bundled typescript Namespace”The TypeScript extension bundles a small .shape source that registers the
typescript namespace (not std::core::typescript) when the extension loads.
This is what makes the following importable:
import { eval, import as ts_import } from typescript
let answer: int = eval("40 + 2")let mod = ts_import("./helpers.ts")eval(code: string) -> _— evaluates a TypeScript/JavaScript expression in the extension’s V8 isolate and marshals the result back to a Shape value.import(specifier: string) -> _— resolves and loads a module in the V8 runtime; the returned handle provides access to the module’s exports.
This bundling mechanism is the get_shape_source vtable field documented in
Polyglot Functions :: Capability Contract.
Troubleshooting
Section titled “Troubleshooting”No language runtime registered for 'typescript'
Section titled “No language runtime registered for 'typescript'”The extension was not loaded in runtime context. Verify
frontmatter / shape.toml / --extension path and rebuild if needed.
Async function never resolves
Section titled “Async function never resolves”async fn typescript blocks until the returned promise settles. If a promise
never resolves (for example, awaiting a fetch with no timeout against an
unreachable host), the Shape VM will hang on the awaiting call. Use
Result<T> and surface a timeout from inside the TypeScript body for
defensive code paths.
Hover works but TypeScript semantic highlighting is missing
Section titled “Hover works but TypeScript semantic highlighting is missing”Shape delegates highlighting to the child TypeScript LSP. If
typescript-language-server is missing or does not implement semantic-token
methods, hover can work while semantic highlighting is unavailable.
Verify child server availability:
typescript-language-server --stdioThe current TypeScript extension advertises this command for child LSP startup.