posit-dev / posit-dev/shinyreact
Strongly typed Inputs/Outputs from TS source of truth
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 14
- Forks
- 3
- Avg merge
- 9h 12m
- Merged PRs (30d)
- 74
Description
Summary
In the SPA-first model, the client decides which inputs exist (useShinyInput<T>(\"name\", default)) and what shape they have. Today the Python server(inputs, output, session) sees those inputs as Any — users can't get IDE completion or type checking for inputs.move_item, even though the JSX side already has the type.
We should generate strongly-typed Inputs / Outputs classes from the TS source so:
def server(inputs: AppInputs, output, session):
move_item: Value[MoveItemInfo] = inputs.move_item # typed, not Any
Why
- JSX is the single source of truth for input names + shapes.
- A Python-authored
TypedDictwould drift the moment someone renames an input in JSX. - We already pay a build step in shadcn-style examples (15, 16) — adding type-gen there is cheap.
- For build-free examples (13, 14) we need a different (weaker) story.
Proposed approach
Primary path — codegen from TS for build-step apps:
A shinyreact typegen CLI (or a Vite plugin) that:
- Walks the project's TS/JSX with the TS Compiler API.
- Finds every
useShinyInput<T>(name, ...)anduseShinyOutput<T>(name)call. - Extracts
name(string literal) andT(TS type). - Emits a Python file (e.g.
app_inputs.py) next toapp.pycontaining:
from shiny.reactive import Value
from typing import Protocol
from .types import MoveItemInfo # also generated, from the TS type
class AppInputs(Protocol):
move_item: Value[MoveItemInfo]
# ...
class AppOutputs(Protocol):
column_data: Value[dict[str, list[str]]]
- Generated values are
Value[T]so they match Shiny's reactive access pattern. - The TS types themselves are converted to Python (TypedDict / Pydantic / dataclass) — likely via
ts-json-schema-generator→ JSON Schema → Python, or a direct walker. - Generated files are committed (same policy as
js/dist/).
Fallback path — Claude skill for build-free apps (13, 14):
For examples that have no JS build step (the JS is inline or a single static file), provide a skill that reads the JS, infers the input/output shapes, and writes app_inputs.py once. No enforcement; drifts on hand edits. Documented as a convenience, not a guarantee.
API shape
from .app_inputs import AppInputs, AppOutputs
def server(inputs: AppInputs, output: AppOutputs, session):
@reactive.effect
@reactive.event(inputs.move_item, ignore_init=True)
def _():
msg: MoveItemInfo = inputs.move_item() # callable, returns the typed value
inputs.move_item is Value[MoveItemInfo] (callable to read, mirrors Shiny's existing input.foo() access).
Open questions
- Dynamic input names (e.g.
useShinyInput(\row_${id}`)): likely an escape-hatchinputs.dynamic["row_42"]: Value[T]` indexer. - Runtime validation: should the generator also emit a runtime schema (Pydantic / msgspec) so a JS↔Python mismatch fails loudly with a helpful error, not just type-check noise?
- Where the generator lives: separate package (
shinyreact-typegen), part ofshinyreactPython package as a CLI entry point, or a Vite plugin injs/? - TS → Python type mapping coverage: unions, discriminated unions, generics,
Date. Probably restrict to JSON-serializable subset and reject the rest with a clear error. - No-TS users: apps that write plain JS (ex 13, 14) get no static info. Is the skill-based fallback acceptable or do we want a runtime-declared alternative?
- Outputs symmetry:
useShinyOutput<T>(name)on the JS side ↔@reactive_outputreturn type on the Python side. Generator should cross-check both directions.
Out of scope (for now)
- R package equivalent.
- Reactive value writes from the JS side beyond what
useShinyInputalready covers.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing build-step examples 15 and 16 with build-free examples 13 and 14, then inspect the existing js/dist/ generated-file policy and app.py layout. Trace useShinyInput and useShinyOutput as the proposed TypeScript entry points. Done means a documented type-generation path emits app_inputs.py with typed Inputs/Outputs for build-step apps and an explicitly documented fallback or decision for build-free apps, with the open mapping and dynamic-name questions resolved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, typescript
- Domain
- build-system, developer-experience, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100