Typerapp is type-safe Hyperapp V2 + α. It's written in TypeScript.
npm install typerapp
Note: Typerapp uses TypeScript source file in node module. If you use Webpack ts-loader, enable allowTsInNodeModules option, and include
node_modules/typerapp
of tsconfig.json. It is not necessary for Parcel.
- Remove
data
argument from Action - Add
dispatch
toview
arguments - Pure DOM Events
Typerapp Action has only two arguments.
Hyperapp:
const Act = (state, { value }, data) => ({...})
Typerapp:
const Act: Action<State, { value: number }> = (state, params) => ({...})
Hyperapp:
app({
view: state => ...
})
Typerapp:
app<State>({
view: (state, dispatch) => ...
})
In Typerapp, specify a function that takes Event as an argument to VDOM event.
Then, call the Action using the dispatch
.
Hyperapp:
const Input = (state, ev) => ({ ...state, value: ev.currentTarget.value })
app({
view: state => <div>
<input
value={state.value}
onInput={Input}
/>
</div>
})
Typerapp:
const Input: Action<State, string> = (state, value) => ({ ...state, value })
app<State>({
view: (state, dispatch) => <div>
<input
value={state.value}
onInput={ev => dispatch(Input, ev.currentTarget.value)}
/>
</div>
})
Type-safe Actions, Effects, Subscriptions, HTML Elements, and more...
Type:
export type ActionResult<S> = S | [S, ...Effect<any, any>[]]
export type Action<S, P = Empty> = (state: S, params: P) => ActionResult<S>
Use:
// without parameter
const Increment: Action<State> = state => ({ ...state, value: state.value + 1 })
// with parameter
const Add: Action<State, { amount: number }> = (state, params) => ({
...state,
value: state.value + params.amount
})
Type:
export type Effect<S, P = Empty> = [(props: P, dispatch: Dispatch<S>) => void, P]
Define Effect:
// Delay Runner Props
export type DelayProps<S, P> = {
action: EffectAction<S, P>
duration: number
}
// Delay Effect Runner
const DelayRunner = <S, P>(props: DelayProps<S, P>, dispatch: Dispatch<S>) => {
setTimeout(() => dispatch(props.action), props.duration)
}
// Delay Effect Constructor
export function delay<S, P>(action: DelayProps<S, P>['action'], props: { duration: number }): Effect<S, DelayProps<S, P>> {
return [DelayRunner, { action, duration: props.duration }];
}
Use:
// Increment with Delay
const DelayIncrement: Action<State> = state => [
state,
delay(Increment, { duration: 1000 })
]
// Add with Delay
const DelayAdd: Action<State, { amount: number }> = (state, params) => [
state,
delay([Add, { amount: params.amount }], { duration: 1000 })
]
Type:
export type Subscription<S, P = Empty> = [(props: P, dispatch: Dispatch<S>) => () => void, P]
Define Subscription:
// Timer Runner Props
export type TimerProps<S, P> = {
action: EffectAction<S, P>
interval: number
}
// Timer Subscription Runner
const timerRunner = <S, P>(props: TimerProps<S, P>, dispatch: Dispatch<S>) => {
const id = setInterval(() => dispatch(props.action), props.interval)
return () => clearInterval(id)
}
// Timer Subscription Constructor
export function timer<S, P>(action: TimerProps<S, P>['action'], props: { interval: number }): Subscription<S, TimerProps<S, P>> {
return [timerRunner, { action, interval: props.interval }]
}
Use:
app<State>({
subscriptions: state => [
timer(Increment, { interval: 1000 })
]
})
Typerapp Html.d.ts forked from React of DefinitelyTyped.
TypeScript is NO check for exceed property on Action.
const Act: Action<State> = state => ({
...state,
typo: 1 // no error!
})
Workaround:
// type alias for Action/ActionResult
type MyAction<P = Empty> = Action<State, P>
type MyResult = ActionResult<State>
// explicit return type
const Act: MyAction = (state): MyResult => ({
...state,
typo: 1 // error
})
For truly solution, please vote Exact Types.
Typerapp has extra features.
actionCreator
is simple modularization function.
// part.tsx
import { h, View, actionCreator } from 'typerapp'
type State = {
foo: string,
part: {
value: number
}
}
const createAction = actionCreator<State>()('part')
const Add = createAction<{ amount: number }>(state => ({
...state,
value: state.value + params.amount
}))
export const view: View<State> = ({ part: state }, dispatch) => <div>
{state.value} <button onClick={ev => dispatch(Add, { amount: 10 })}>request</button>
</div>
ActionParamOf
type gets parameter type of Action from Effect/Subscription Constructor.
import { ActionParamOf } from 'typerapp'
import { httpJson } from 'typerapp/fx'
// { json: unknown }
type ParamType = ActionParamOf<typeof httpJson>
const JsonReceived: Action<State, ParamType> = (state, params) => ({
...state,
text: JSON.stringify(params.json)
})
Helmet
renders to the head element of DOM.
import { Helmet } from 'typerapp/helment'
app<State>({
view: (state, dispatch) => <div>
<Helmet>
<title>{state.title}</title>
</Helmet>
</div>
})
Recommend performance improvement with Lazy:
const renderHead = (props: { title: string }) => <Helmet>
<title>{props.title}</title>
</Helmet>
app<State>({
view: (state, dispatch) => <div>
<Lazy key="head" render={renderHead} title={state.title} />
</div>
})
Typerapp Router is url-based routing Subscription. Router syncs URL to the state by History API.
import { createRouter, Link, RoutingInfo, Redirect } from 'typerapp/router'
// Update routing
const SetRoute: Action<State, RoutingInfo<State, RouteProps> | undefined> = (state, route) => ({
...state,
routing: route,
})
// Create router
const router = createRouter<State, RouteProps>({
routes: [{
title: (state, params) => 'HOME',
path: '/',
view: (state, dispatch, params) => <div>home</div>,
}, {
title: (state, params) => 'Counter / ' + params.amount,
path: '/counter/:amount',
view: (state, dispatch, params) => {
const amount = params.amount ? parseInt(params.amount, 10) : 1
return <div>
<div>{state.value}</div>
<button onClick={ev => dispatch(Add, { amount })}></button>
</div>
},
}, {
title: (state, params) => 'Redirect!',
path: '/redirect',
view: (state, dispatch, params) => <Redirect to="/" />,
}],
matched: (routing, dispatch) => dispatch(SetRoute, routing),
})
app<State>({
view: (state, dispatch) => <div>
<div><Link to="/">Home</Link></div>
<div><Link to="/Counter/10">Count10</Link></div>
<div><Link to="/Redirect">Redirect</Link></div>
{
state.routing
? state.routing.route.view(state, dispatch, state.routing.params)
: <div>404</div>
}
</div>
})
Typerapp style
forked from Picostyle.
import { style } from 'typerapp/style'
// styled div
const Wrapper = style('div')({
backgroundColor: 'skyblue',
width: '50px',
})
// styled div with parameter
const StyledText = style<{ color: string }>('div')(props => ({
color: props.color,
transition: "transform .2s ease-out",
":hover": {
transform: "scale(1.5)",
},
"@media (orientation: landscape)": {
fontWeight: "bold",
},
}))
app<State>({
view: (state, dispatch) => <div>
<Wrapper>
<StyledText color="green">text</StyledText>
</Wrapper>
</div>
})
TypeScript is no check for hyphened attributes on TSX.
Please import typerapp/main/svg-alias
for type checkable camel-case attributes.
In the below, strokeWidth
and strokeDasharray
is converted to stroke-width
and stroke-dasharray
.
import "typerapp/main/svg-alias"
<svg x="0px" y="0px" width="200px" height="3" viewBox="0 0 200 1">
<line
x1="0"
y1="0.5"
x2="200px"
y2="0.5"
stroke="skyblue"
strokeWidth={3}
strokeDasharray={5}
/>
</svg>
In Typerapp, if your Effect/Subscription returns a value by Action, you must merge a return value into Action parameter, because Typerapp has not data
of Action.
In that case, you can use mergeAction
function.
import { EffectAction, Dispatch, Effect } from "typerapp"
import { mergeAction } from 'typerapp/fx/utils'
export type RunnerProps<S, P> = {
action: EffectAction<S, P, { returnValue: number }>
}
const effectRunner = <S, P>(props: RunnerProps<S, P>, dispatch: Dispatch<S>) => {
dispatch(mergeAction(props.action, { returnValue: 1234 }))
}
export function effect<S, P>(action: RunnerProps<S, P>["action"]): Effect<S, RunnerProps<S, P>> {
return [effectRunner, { action }]
}