pyRPC generates a TypeScript declaration file describing your Python procedures. Where that file lives is a product decision, not an implementation detail. For a long time it lived innode_modules/@pyrpc/types/src/index.ts, like the type package it replaced. The source-tree model moves it to <client>/__pyrpc.d.ts, a file inside your frontend, tracked in git alongside everything else.
The node_modules problem
Writing generated types into node_modules worked, but it was structurally wrong in three ways:
- Non-durable.
node_modulesis disposable by contract. Anynpm install, any CI checkout that skips it, anynpm cirun, and your types are gone, regenerated from a cold start or, worse, silently stale. - Not reviewable. You cannot diff a generated file you never committed. Changes to procedure signatures flew past without showing up in pull requests.
- Confusing ownership. A file that belongs to a dependency directory reads as someone else’s code, even though pyRPC wrote it. Debuggers, linters, and teammates all misattribute it.
The source-tree contract
The new model treats generated types like any other source file. There is one convention and it is enforced in one place:
_DEFAULT_CLIENT = "." # ...in _regenerate_clients: output_path = os.path.abspath(os.path.join(client_dir, "__pyrpc.d.ts"))
Types land at <client>/__pyrpc.d.ts, relative to the client root configured in pyrpc.json. The .d.ts extension means TypeScript treats it as a declaration file; the leading double-underscore keeps it visually separate from your own modules and immune to tsc glob collisions with your src tree.
The file’s header makes the contract explicit:
/**
* Auto-generated by pyrpc dev, do not edit by hand.
* Re-generated automatically whenever your Python procedures change.
*
* Resolved via tsconfig.json paths alias:
* "paths": { "@pyrpc/types": ["./__pyrpc.d.ts"] }
*/Committed, diffed, trusted
Because the file is committed, it becomes part of your review loop:
- Renames show up. Rename
get_usertofetch_useron the server and the next regeneration produces a visible diff in__pyrpc.d.ts, the frontend change is now an explicit review point. - CI is deterministic. A fresh checkout has your types from git immediately; the watcher only needs to run to update them.
- No reinstall races. Nothing about the workflow depends on npm having run, or on
postinstallscripts having fired.
How imports still resolve
Your frontend code never imports ./__pyrpc.d.ts directly. It writes the stable import, and the tsconfig alias redirects it:
import type { Types } from "@pyrpc/types"
// tsconfig.json
"compilerOptions": {
"paths": {
"@pyrpc/types": ["./__pyrpc.d.ts"]
}
}The @pyrpc/types package still exists on npm, it ships a placeholder and the ProcedureKinds constant used by the framework adapters. But for yourprocedures, TypeScript resolves the alias to the generated file in your tree, so the published package’s placeholder is never what you actually type-check against.
What you gain
The source-tree model is the quiet foundation of everything else: multi-client support works because each client simply owns its own committed __pyrpc.d.ts; the tsconfig injection works because there is a fixed, predictable path to alias; CI works because nothing depends on ephemeral install state. It is the difference between generated code you tolerate and generated code you treat as part of your codebase, which is exactly how the best typed-contract tools treat their output.
Read the full changelogfor the complete list of changes.

pyRPC