TypeScriptでHyperappを書く

作成日: 2018/1/13, 更新日:

更新履歴

はじめに

この記事はTypeScript2.xを使用して、Hyperappを使ったアプリケーションを書くためのドキュメントである。

HyperappのドキュメントにはTypeScriptでの使い方が書いていないため書いた。
まぁTS型定義のテストファイル見れば使い方わかるんだけどね...

この記事で書かれているコードの確認環境は以下の通り

また、仮想ノードの構築にはJSX(.tsx)を用いる。

基本編

Stateを書く

まず最初にstateの型を定義する必要がある。
単純にstateの型をそのまま書いてやればよい。

interface State {
  count: number
}

const state: State = {
  count: 0
}

Actionsを書く

次にactionsの型を定義する。
各actionの型は「入力を受け取ってstateを返す」関数として定義する。

interface Actions {
  increment(): State
  decrement(value: number): State
}

そして定義した型を基にactionsを実装する。
実装側はActionsType型を使うため、input? => (state?, actions?) => resultとなるようにする。
resultの部分の型定義は公式の型定義ファイルを参照してね。

import { ActionsType } from 'hyperapp'

const actions: ActionsType<State, Actions> = {
  increment: () => (state, actions) => ({ count: state.count + 1 }),
  decrement: (value: number) => (state, actions) => ({ count: state.count - value }),
}

...しかし、残念なことに現時点では定義したActionsの型情報があまり役に立たたない。

// このように書いてもTSのコンパイルは通ってしまう
const actions: ActionsType<State, Actions> = {
  increment: (value: number) => (state, actions) => ({ count: value }),
  decrement: (value: string) => (state, actions) => ({ count: value })
}

actionsの実装に於いてはアクション名のチェック程度にしか使えないと考えておいたほうがよい。

Viewを書く

ロジックの次はviewを書く。特に難しいことは無い。

import { h, View } from 'hyperapp'

const view: View<State, Actions> = (state, actions) => (
  <main>
    <button onclick={() => actions.decrement(1)}>-</button>
    <span>{state.count}</span>
    <button onclick={() => actions.increment()}>+</button>
  </main>
)

ジェネリクスパラメータで指定しているStateActionsがここではちゃんと活きている。

// これはコンパイルエラーになる
const view: View<State, Actions> = (state, actions) => (
  <main>
    <button onclick={() => actions.decrement('1')}>-</button>
    <span>{state.count}</span>
    <button onclick={() => actions.increment(1)}>+</button>
  </main>
)

がっちゃんこ

最後にstate, actions, viewをがっちゃんこする。
ジェネリクス指定してあげる以外はTS的な要素はない。

import { app } from 'hyperapp'

app<State, Actios>(
  state, actions, view, document.getElementById('app')
)

応用編

モジュール化する

ここでいう「モジュール化」とはstateとactionsをグループ化することであり、ESModulesのことではない。

直感的にそのまま分ければ大丈夫。

// modules/counter.ts
import { ActionsType } from 'hyperapp'

export interface State { count: number }
export const state: State = { count: 0 }

export interface Actions {
  increment(): State
  decrement(value: number): State
}
export const actions: ActionsType<State, Actions> = {
  increment: () => state => ({ count: state.count + 1 })
  decrement: (value: number) => state => ({ count: state.count - value })
}
// index.ts
import * as Count from './modules/counter'

interface State { count: Count.State }
const state: State = { count: Count.state }

interface Actions { count: Count.Actions }
const actions: ActionsType<State, Actions> = {
  count: Count.actions
}

これでstate.count.countactions.count.incrementとしてアクセスできるようになる。

Componentを書く

コンポーネントは至ってシンプル。

// components/Counter.tsx
import { h, Component } from 'hyperapp'

interface Props {
  count: number
  onchange(v: number): any
}

const Counter: Component<Props> = ({ count, onchange }) => (
  <div>
    <button onclick={() => onchange(count - 1)}>-</button>
    <span>{count}</span>
    <button onclick={() => onchange(count + 1)}>+</button>
  </div>
)

export default Counter

ただ、このまま書いていくとpropsのバケツリレーが起きてしまう。そのため、Lazy Componentsという機能を使い、StateとActionsを直接コンポーネントに渡して冗長さを軽減することができる。

// ...

import { State, Actions } from '../'

// Component<Props, State, Actions>なので、propsを受け取らない場合は{}を指定する
const Counter: Component<{}, State, Actions> = () => (state, actions) => (
  <div>
    <button onclick={actions.count.decrement}>-</button>
    <span>{state.count.count}</span>
    <button onclick={actions.count.increment}>+</button>
  </div>
)

// ...

おわりに

以上Hyperappの各要素にフォーカスした書き方でした。

実際のサンプルが見たい場合はTypeScript+Hyperapp(+CSS Modules) build with Webpackなサンプルプロジェクトを用意しているので、そちらを見てね。