posit-dev / posit-dev/shinyreact

Strongly typed Inputs/Outputs from TS source of truth

Open
#30 0 comments 0 reactions 0 assignees View on GitHub

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 TypedDict would 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:

  1. Walks the project's TS/JSX with the TS Compiler API.
  2. Finds every useShinyInput<T>(name, ...) and useShinyOutput<T>(name) call.
  3. Extracts name (string literal) and T (TS type).
  4. Emits a Python file (e.g. app_inputs.py) next to app.py containing:
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-hatch inputs.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 of shinyreact Python package as a CLI entry point, or a Vite plugin in js/?
  • 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_output return 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 useShinyInput already covers.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.