Edited at

hyperappで大きめなアプリをTypeScriptで書くには?

More than 1 year has passed since last update.

こんな感じにすれば良さそう

import { h, app, ActionsType, View } from "hyperapp";

namespace Counter {
export interface State {
count: number;
}

export interface Actions {
down(): State;
up(): State;
}

export const state: State = {
count: 0
};

export const actions: ActionsType<State, Actions> = {
down: () => state => {
if (state.count > 0) {
return { count: state.count - 1 };
}
return state;
},
up: () => state => ({
count: state.count + 1
})
};
export const view: View<State, Actions> = (state, actions) => (
<div>
<div>{state.count}</div>
<button onclick={actions.down}>-</button>
<button onclick={actions.up}>+</button>
</div>
);
}

namespace CounterTwice {
export interface State extends Counter.State {}
export interface Actions extends Counter.Actions {}
export const state: State = Counter.state;
export const actions: ActionsType<State, Actions> = {
down: () => state => {
if (state.count > 0) {
return { count: state.count - 2 };
}
return state;
},
up: () => state => ({
count: state.count + 2
})
};
export const view = Counter.view;
}

namespace MultiCounter {
export interface State {
c1: Counter.State;
c2: CounterTwice.State;
c3: Counter.State;
}
export interface Actions {
c1: Counter.Actions;
c2: CounterTwice.Actions;
c3: Counter.Actions;
alldown(): void;
allup(): void;
}

export const state: State = {
c1: Counter.state,
c2: CounterTwice.state,
c3: Counter.state
};

export const actions: ActionsType<State, Actions> = {
c1: Counter.actions,
c2: CounterTwice.actions,
c3: Counter.actions,
allup: () => (state, actions) => {
actions.c1.up();
actions.c2.up();
actions.c3.up();
return;
},
alldown: () => (state, actions) => {
actions.c1.down();
actions.c2.down();
actions.c3.down();
return;
}
};
export const view: View<MultiCounter.State, MultiCounter.Actions> = (
state,
actions
) => (
<div>
{Counter.view(state.c1, actions.c1)}
{CounterTwice.view(state.c2, actions.c2)}
{Counter.view(state.c3, actions.c3)}
<button onclick={actions.alldown}>-</button>
<button onclick={actions.allup}>+</button>
</div>
);
}

const main = app<MultiCounter.State, MultiCounter.Actions>(
MultiCounter.state,
MultiCounter.actions,
MultiCounter.view,
document.body
);

// hot reload対策
if ((window as any).isSet === undefined) {
setInterval(main.c3.up, 250, 1);
setInterval(main.c3.down, 500, 1);
}
(window as any).isSet = true;

ハマリポイントはallup: () => (state, actions) のところくらいです。

引数にactionsを省略すると補完が効かなくなり悩みました。


routerもTypeScriptから使いたい

大きめのアプリを書くにはrouterも必要そうですよね。

現状本家のrouterの型情報がないみたいなので作りました

mytypes/@hyperapp/router.d.tsに配置して下さい。


mytypes/@hyperapp/router.d.ts


import { VNode } from "hyperapp";

/** Link */
interface LinkProps {
to: string;
location?: Location;
}
export function Link(props: LinkProps): VNode<LinkProps>;

/** Route */
interface Match<P> {
url: string;
path: string;
isExact: boolean;
params: P;
}
interface RenderProps<P> {
location: Location;
match: Match<P>;
}

interface RouteProps<P> {
parent?: boolean;
path: string;
location?: Location;
render: (props: RenderProps<P>) => VNode<RenderProps<P>>;
}

export function Route<P>(
props: RouteProps<P>
): VNode<RenderProps<P>> | undefined;

/**Switch */
export function Switch<P>(
props: object,
children: Array<VNode<Match<P>>>
): VNode<object>;

/** Redirect */
type RedirectProps = LinkProps;
export function Redirect(props: RedirectProps): VNode<RedirectProps>;

/** location */
interface LocationState {
pathname: string;
previous: string;
}

interface LocationActions {
go: (pathname: string) => void;
set: (data: LocationState) => LocationState;
}
interface RouterLocation {
state: LocationState;
actions: LocationActions;
subscribe: (actions: LocationActions) => Function;
}

export declare const location: RouterLocation;


tsconfigに自作の型情報を読むための設定を追加します

{

"compilerOptions": {
"strict": true,
"jsx": "react",
"jsxFactory": "h",
"baseUrl": ".",
"sourceMap": true,
"paths": {
"*": ["mytypes/*", "node_modules/@types/*", "*"]
}
}
}

parcelからtypescript + hyperappが使えますが、

"sourceMap": true,にすることで、TypeScriptデバッグできるようにもなっているみたいです。


routerの書き方

react-routerとおんなじ感じです

sample-code

import { h, app, View, ActionsType } from "hyperapp";

import {
Link,
Route,
RenderProps,
location,
LocationActions,
LocationState
} from "@hyperapp/router";

const Home = () => <h2>Home</h2>;
const About = () => <h2>About</h2>;
const Topic = ({ match }: RenderProps<{ topicId: string }>) => (
<h3>{match.params.topicId}</h3>
);
const TopicsView = ({ match }: RenderProps<{ topicId: string }>) => (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/single-state-tree`}>Single State Tree</Link>
</li>
<li>
<Link to={`${match.url}/routing`}>Routing</Link>
</li>
</ul>

{match.isExact && <h3>Please select a topic.</h3>}

<Route parent path={`${match.path}/:topicId`} render={Topic} />
</div>
);

interface RouteState {
location: LocationState;
}
const state: RouteState = {
location: location.state
};

interface RouteActions {
location: LocationActions;
}

const routeActions: ActionsType<RouteState, RouteActions> = {
location: location.actions
};
const view: View<RouteState, RouteActions> = (state: RouteState) => (
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>

<hr />

<Route path="/" render={Home} />
<Route path="/about" render={About} />
<Route parent path="/topics" render={TopicsView} />
</div>
);

const main = app(state, routeActions, view, document.body);

const unsubscribe = location.subscribe(main.location);


最後に

hyperappはREADMEだけで使いかたが完結していて凄く使いやすそうです。

READMEを翻訳してみたので参照してみて下さい