04 · Adoption

Can I use my existing TypeScript, JavaScript, and npm libraries?

Yes. Glyph compiles to ordinary TypeScript and rides npm: a .glyph module imports from any package and is itself importable from your existing .ts. You adopt it one file at a time.

Why it matters

Nobody rewrites a codebase to try a language. Because Glyph's output is TypeScript, a .glyph file drops into the corner of an existing TS/React/Node project, imports the same packages, and exports back to the .ts around it. That gradual path is the whole reason to target TypeScript instead of shipping a new runtime.

See it

You import packages by name (including hyphenated and npm-scoped ones) and Glyph emits the import you'd expect:

form.glyph
import react { useState }
import react-hook-form { useForm }
import @hookform/resolvers/zod {
  zodResolver,
}
form.ts: emitted
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver }
  from "@hookform/resolvers/zod";

If the package is installed and ships its own types (or has an @types/...), that is all you do. No stub, no adapter file. glyph build finds your project's node_modules and points the type-checker at it, so the package's real types check your code and a wrong call is a real error. Subpath imports (pkg/sub) resolve through the package's exports map too, so a subpath's own types are checked. Here is zod used inline, with nothing hand-written:

main.glyph
import zod { z }

let user_schema = z.object({
  name: z.string(),
  age: z.number(),
})
let user = user_schema.parse(input)
print(user.name)
a call zod doesn't define
z.string().nonexistent_method()

// [TS2339] Property 'nonexistent_method'
//   does not exist on type 'ZodString'.
//   mapped back onto main.glyph:7

glyph run executes the same program against the installed zod, so it is not only a type-check: the one config entry that resolves the package for the type-checker resolves it for the runtime too. A package that ships no types and has no @types/... still needs a declaration you write, which is what the .types/ folder is for (the same .d.ts mechanism TypeScript already uses). And when you want a library's shape as a first-class, validated Glyph type rather than a type-only import, glyph gen dts materializes a .d.ts into real descriptor-bearing types: see do I have to hand-write a validator for every API?

Value-derived types are first-class. type User = z.infer<typeof user_schema> works directly: typeof user_schema is a type query over a real value reference (a typo is an unresolved-name error), and z.infer<...> is an ordinary member-generic type. It emits verbatim and tsc reduces it, so u.name is a string; validation comes from the schema's own parse, which is how zod already works. JSX prop spread (<input {...register()} />) is first-class too. For the rare TypeScript idiom Glyph still can't spell, extern_ts("...") is the scoped, greppable escape hatch: tsc checks it, but be precise, it is a trust boundary, not a runtime-validated one (opaque to Glyph, a value comes back unknown to narrow). So a library never forces a hand-written adapter file.

When you need whole hand-written TypeScript

Sometimes the escape hatch isn't a type or an expression but real runtime code: a node-stream loop, a new Promise, a worker thread. Put the .ts under <src>/extern/ and reach it with import extern/<name>.

// src/extern/raw_server.ts   (hand-written TypeScript)
export function serve_raw(port: number, handler: (raw: string) => string): void { ... }

// src/main.glyph
import extern/raw_server { serve_raw }   // typed from the .ts; a wrong arg is a real tsc error

The build stages <src>/extern/** into the output and emits a relative specifier, so tsc type-checks the extern together with your Glyph code and a rebuild never prunes it. Relative imports stay illegal in Glyph source (a greppability rule), so extern/* is the one reserved path, and grep -rn 'import extern/' finds every place you left the language. It is the inverse of the easy direction, a .ts file importing your compiled Glyph, which needs nothing at all. A common extern (an http server, fs) type-checks against the bundled node shim with nothing installed.

Watching a subprocess, with nothing installed

The bundled node declarations carry the async half of child_process, not just the blocking one, so a tool that reports a long command's output while it runs is Glyph rather than an extern:

module main

import child_process { spawn }
import std/io

pub async fn main(argv: Array<string>) -> number {
  let child = spawn("git", ["status", "--short"])

  // Ask for text and node decodes for you, holding back a character whose
  // bytes straddle two chunks. Decoding a raw chunk yourself does not.
  child.stdout.setEncoding("utf8")
  child.stdout.on("data", fn(chunk) {
    io.print("${chunk}")
  })
  child.on("close", fn(code) {
    io.println("git exited ${code}")
  })
  return 0
}

io.print and not io.println: a chunk is a slice of the stream, not a line, and one line can arrive across two of them. The overloads follow @types/node, so the pipes come back non-null from a plain spawn(command, args) and nullable once you pass stdio. Reading child.stdout after stdio: "inherit" is TS18047 here and TS18047 with the real typings, rather than two different stories about the same value.

Edit the shim, and the next run is the shim you edited

A shim is a file you iterate on, so it has to count as a source. glyph run caches a build under a fingerprint of your sources, and that fingerprint covers every .ts and .tsx under extern/, by path as well as by contents. Edit one, rename one, add one, delete one, and the next run rebuilds and re-runs tsc. A symlinked shim is followed, so a .ts kept outside the source tree is hashed by what it actually contains.

That is what lets the shim own a type instead of both files declaring their own copy of it. Export the shape from the shim, import it in the Glyph module, and the compiler holds the two together on every run:

src/extern/web.ts
export type WebResponse = {
  status: number;
  content_type: string;
  location: string;   // "" when not a redirect
  body: string;
};
the shim renamed a field, the build says so
import extern/web { serve_web, WebResponse }

// location -> redirect_to in web.ts, nothing
// touched here, and glyph run stops:
[TS2353] Object literal may only specify
known properties, and 'location' does not
exist in type 'WebResponse'.
   ╰─ server:53:3

Until 0.1.44 the fingerprint skipped extern/ entirely, so that rename produced a clean, type-checked build running the previous copy of the shim. Files under extern/ that are not .ts or .tsx still do not affect the cache, which is deliberate: a README.md next to your shim is copied into the output along with it, but nothing type-checks or runs it, so there is nothing to rebuild.

Class-based clients: the database and messaging ones

Most database and messaging clients are class-based: you write new Kafka(...), new MongoClient(url), new Redis(), new Pool(cfg). Glyph has new for exactly this. It emits a verbatim TypeScript new and tsc checks it against the package's real constructor, so a wrong argument is a real error and an undefined callee is caught by name. It stays interop-only: Glyph has no class of its own, and new only constructs a type that comes from a package, a .types declaration, or extern_ts.

a class-based client (kafkajs)
import kafkajs { Kafka }

let kafka = new Kafka({
  clientId: "app",
  brokers: ["localhost:9092"],
})
let producer = kafka.producer()
await producer.connect()
a factory client needs no new
import redis { createClient }

// createClient() is a function, so you
// just call it. No new, no wrapper.
let client = createClient()
await client.connect()
await client.set("k", "v")

Where it stands

Shipping today

Import any npm package (hyphenated and scoped names included). An installed package that ships its own types (or has @types/...) type-checks and runs with no stub. Class-based clients construct with new, checked by tsc against the real constructor; factory clients import and call directly. Node builtins (fs, path, http, child_process including the streaming spawn, and the rest) type-check out of the box, with @types/node loaded automatically when you install it, and CI builds the compiler's own runtime against that package at latest, so a declaration the stdlib depends on cannot drift from node's real one. A declaration only your code reaches is not covered by that check yet, and a few are still wider than node. .types/ ambient declarations for anything else, glyph gen dts to materialize a .d.ts into validated Glyph types, and clean .ts output your existing TypeScript imports back. When you need whole hand-written TypeScript, import extern/<name> reaches a .ts under <src>/extern/, staged and type-checked with your code, and tracked by the glyph run cache so editing it rebuilds.