Hyperapp with TypeScriptプロジェクトのサンプル

## はじめに

[Hyperapp](https://github.com/jorgebucaran/hyperapp)バージョン2が着々と作られる昨今、今更ながら入門をしたんですが、「[TypeScript](https://www.typescriptlang.org/)の型付けどうしよう？」とか「ディレクトリ構造どうしよう？」とか、色々悩むことがありました。格闘した結果、「とりあえず、こんくらいでええやろ」というのができたので共有します。

[公式のGetting Started](https://github.com/jorgebucaran/hyperapp#getting-started)をJavaScriptからTypeScriptに替えて整理した簡単なものです。

能書きはいいからさっさとソース見せろという人はこちらをご覧ください。
https://github.com/babie/hyperapp-typescript-sample/tree/61a81b547d3734673f633e3d67cd19d702d2b693
Gitのログがあっさりしてるけども、プライベートリポジトリからファイルコピーしてきたんでこうなってます。あしからず。


## ターゲット

HyperappウェブアプリをTypeScriptで書きたい人向けで、基本的なHyperappやTypeScriptの知識はあるものとします。


## 環境

```
macOS Mojave: 10.14.1（18B75）
Node.js: 10.13.0(lts/dubnium)
Yarn: 1.12.3
```
で、お送りします。Nodeはv6でも動くやろ、きっと。


## 概観

ざっと、プロジェクトのディレクトリ構造でも見ときましょうか。

```shell-session
$ tree -a -I '.git|node_modules'
.
├── .cache
│   ├── 00
│   ├── ...
│   └── ff
├── .gitignore
├── LICENSE
├── README.md
├── build
│   ├── index.html
│   └── src.214b09e4.js
├── dist
│   ├── index.html
│   ├── src.77de5100.js
│   └── src.77de5100.map
├── package.json
├── src
│   ├── Root.tsx
│   ├── components
│   │   ├── About.tsx
│   │   ├── Home.tsx
│   │   └── index.ts
│   ├── index.html
│   ├── index.ts
│   └── modules
│       ├── counter.ts
│       ├── index.ts
│       └── locator.ts
├── tsconfig.json
├── tslint.json
├── types
│   └── @hyperapp-logger
│       └── index.d.ts
└── yarn.lock
```

これ見せるだけで、解説の8割終わった。要は、state/actionsの詰まった`modules`と、JSXの入った`components`に分けたってことですね。`components`が`views`になったり、Atomic Designの配置になったりはあるでしょうね。まぁ、ざっくりこんな感じにしましたという報告です。

作業してるとこんな感じになります。`build`がプロダクション出力、`dist`が開発用出力、`.cache`はバンドラーのキャッシュ入れです。これらは`.gitignore`に追加した方が良い。

TypeScriptに慣れてない方は「おやおや〜、`types`ってなんだ〜？」と思われるでしょうか。`@hyperapp/logger`にTypeScript用型定義ファイルがないので、自分のところに置いてます。中身は[このプルリクエスト](https://github.com/okwolf/hyperapp-logger/pull/25)から取ってきた。うんうん、これもまたアイカツだね。


## 設定ファイル

### パッケージ

いきなり`package.json`を貼る。

```json:package.json
{
  "name": "hyperapp-typescript-sample",
  "scripts": {
    "start": "parcel src/index.html",
    "build": "parcel build src/index.html --out-dir build --no-source-maps --experimental-scope-hoisting",
    "clean": "rm -rf .cache dist build",
    "lint": "lynt src"
  },
  "dependencies": {
    "@hyperapp/logger": "^0.5.0",
    "@hyperapp/router": "^0.7.1",
    "hyperapp": "^1.2.9"
  },
  "devDependencies": {
    "@types/parcel-env": "^0.0.0",
    "lynt": "^0.5.4",
    "parcel": "^1.10.3",
    "prettier": "^1.15.2",
    "typescript": "^3.1.6"
  },
  "private": true,
  "lynt": {
    "typescript": true,
    "react": true,
    "ignore": ["tests/**/*.*", "fixtures/**/*.*"],
    "rules": {
      "jsx-no-lambda": "off"
    }
  },
  "prettier": {
    "semi": false,
    "singleQuote": true
  }
}
```

バンドラーに[Parcel](https://parceljs.org/)、リントに[Lynt](https://github.com/saadq/lynt)、整形に[Prettier](https://prettier.io/)を使用してますね。趣味です。まぁ、Parcelで動けば[Webpack](https://webpack.js.org/)でも動きます。逆はそうでもない。

Parcel/Lyntはどちらも設定ファイル要らないんですが、外観にあるように、`tsconfig.json`と`tslint.json`を置いてあります。これはコードで型などを間違ったときにVSCodeの拡張機能から怒ってもらいたいからです。怒られると、気持ちいい。

肝心のHyperapp関連ですが、本体`hyperapp`の他に[@hyperapp/router](https://github.com/hyperapp/router)と[@hyperapp/logger](https://github.com/okwolf/hyperapp-logger)を入れてます。その名の通り、ルーティングとログのためですね。

Yarnスクリプトですが、

* `yarn start`が開発用ビルドとサーバーの起動
* `yarn build`がプロダクション用ビルド
* `yarn clean`が上記の掃除（yarn.lock/node_modulesは消さない）
* `yarn lint`がリント

です。

Lyntの設定で`jsx-no-lambda`がありますが、Reactの場合JSXで無名関数使うと、無名関数は毎回違うidを差し出し差分が発生するため怒られるんですが、Hyperappの場合はまぁ、無名関数使わずに関数内関数？かなんか書いてbindかなんかして？なんとかする方が良いのでしょうけど、[公式のGetting Started](https://github.com/jorgebucaran/hyperapp#getting-started)に書いてあるし、ダルいのでオフにしてあります。

Prettierの設定では、セミコロンを無しに、文字列はシングルクォートになるようにしてます。[JavaScript Standard Style](https://standardjs.com/)派なので。これからの正義の話をしよう。

まぁ、何はなくとも、`yarn install`してください。

### TypeScriptの設定

```json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["es5", "dom"],
    "jsx": "react",
    "jsxFactory": "h",
    "outDir": "./build",
    "strict": true,
    "baseUrl": "./",
    "paths": { "@hyperapp/*": ["types/@hyperapp-*"] },
    "typeRoots": ["types", "node_modules/@types"],
    "esModuleInterop": true 
  },
  "compileOnSave": true,
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"]
}
```

Hyperappに関するポイントだけ。

`jsx`を`react`に`jsxFactory`を`h`にすれば、TypeScriptでJSXが使えます。`jsx`はまぁデフォルトの`preserve`でもいいんですが、これだと変換された後のファイル拡張子が`.jsx`になってナウくないので`.js`になるようにしてます。

`baseUrl`/`paths`/`typeRoots`はさっきちょろっと書いた`@hyperapp/logger`が型定義を持ってないからですね。この中でも`paths`が特に大事で`import`の`from`句で指定されるパッケージ名の書き換えみたいなもんですね。この場合、`import { foo } from '@hyperapp/bar'`したときに、`types/@hyperapp-bar`に型定義を探しに行ってくれます。普通はこんな設定しなくても、プロジェクトのどこかに`types`ってディレクトリ作ったら勝手に見てくれるんですけど、定義ファイルを`types/@hyperapp/logger/index.d.ts`にディレクトリ深く置いたら、「`@hyperapp`の定義がねーよ」って怒られるので、こういう形になりました。

あとは今回の件とはあんま関係ないので、気になったら別で調べてください。


## ソース解説

### エントリポイント

HTMLです。

```html:src/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Welcome to Hyperapp</title>
    <meta name="description" content="Hyperapp with TypeScript Sample Page." />
  </head>
  <body>
    <script src="./index.ts"></script>
  </body>
</html>
```

なんてことはないHTMLですね。`src/index.ts`を取り込みます。相対パスになってますが、ビルド時にバンドラーがよろしくやってくれます。便利ですね。

ソースのルートです。

```ts:src/index.ts
import { app } from 'hyperapp'
import { location } from '@hyperapp/router'

import { Modules as M } from './modules'
import { Root as R } from './Root'

let main
if (process.env.NODE_ENV === 'development' && module.hot) {
  main = require('@hyperapp/logger').withLogger(app)(
    M.state,
    M.actions,
    R.view,
    document.body
  )
} else {
  main = app(M.state, M.actions, R.view, document.body)
}
const unsubscribe = location.subscribe(main.location)
```
まぁ、細かく解説はしなくてもええやろ。ここではTypeScriptらしさとかないし。HyperappもHyperapp Routerも公式の使い方通りです。

問題は、`Modules`と`Root`やね。`Modules`はHyperappに渡すステートとアクションが詰めたものです。次見る。`Root`の方はJSXのルートノードを詰めたものです。`Root.view`がCreate React Appなんかでは、`<App />`相当のやつ。`App`だとHyperappの`app`やその型`App`と被るので`Root`としました。まぁ、何でも良い。

### モジュール

```ts:src/modules/index.ts
import { Counter } from './counter'
import { Locator } from './locator'

export namespace Modules {
  export type AppState = Counter.IState & Locator.IState
  export type AppActions = Counter.IActions & Locator.IActions
  export const state: AppState = { ...Counter.state, ...Locator.state }
  export const actions: AppActions = { ...Counter.actions, ...Locator.actions }
}
```

ゲーッ！`namespace`！交差型！オブジェクトスプレッド……は普通か。

交差型は複数のインターフェースやタイプエイリアスをマージするやつね。メンバが合成される。ちなみに同じ名前・同じ型のメンバがそれぞれにあっても、コンパイラに怒られない。なにそれこわい。`IProps`はその名の通り`interface`なんだけど、`type`にしても別に怒られは発生しない。んー、どっかで見たTypeScript情報ではエラーが発生するって書いてあった気がするんだがな。なので注意なのです。

`namespace`はTypeScript使ってる界隈でもあんま推奨されてないんだが、私はJSで`export`するときに`object`をコンテナとして詰めて送り出すのが好きというのがまずあり、TypeScriptでそれをやろうと思ったら、インターフェースを詰められるものが`namespace`ぐらいしかなかったというわけ。まぁ、字面的にもオブジェクトよりはこれを使うのがふさわしいやろ。おっけー。

```ts:src/modules/counter.ts
export namespace Counter {
  export interface IState {
    count: number
  }
  export const state: IState = {
    count: 0
  }

  export interface IActions {
    down: (value: number) => (state: IState) => IState
    up: (value: number) => (state: IState) => IState
  }
  export const actions: IActions = {
    down: (value: number) => (state: IState) =>
      ({ count: state.count - value } as IState),
    up: (value: number) => (state: IState) =>
      ({ count: state.count + value } as IState)
  }
}
```

なんか解説しようと思ったけど、型付けて送り出すと間違った使い方したときに怒られが発生して良い以上のことはないな。普通の型付け。`interface`を`type`にしてもいいんだけど、TSLintに`[tslint] Use an interface instead of a type literal. [interface-over-type-literal]`って怒られます。どうしてもタイプエイリアス使いたい人はTSLint設定で、`interface-over-type-literal`をオフにしてください。

あっ、そういえば、Hyperappの機能に、[Nested Actions](https://github.com/jorgebucaran/hyperapp#nested-actions)ってのがあるんですけど、TypeScriptで型付けたらできないから。それだけ。

```ts:src/modules/locator.ts
import { location, LocationState, LocationActions } from '@hyperapp/router'

export namespace Locator {
  export interface IState {
    location: LocationState
  }
  export const state: IState = {
    location: location.state
  }

  export interface IActions {
    location: LocationActions
  }
  export const actions: IActions = {
    location: location.actions
  }
}
```
Hyperapp Router公式のUsageをファイル配置を変えて型付けただけです。あ、importで型持ってきてるね。型ファイルはさっき言ってたプルリクエストのやつ。

### コンポーネント

んで、まずはルーティングです。ソースのルートで呼び出してた`Root`が書いてあるやつ。

```tsx:src/Root.tsx
import { h, View } from 'hyperapp'
import { Switch, Route } from '@hyperapp/router'
import { Modules as M } from './modules'
import { Components as C } from './components'

const view: View<M.AppState, M.AppActions> = () => (
  <Switch>
    <Route path="/" render={C.Home} />
    <Route path="/about" render={C.About} />
  </Switch>
)

export const Root = {
  view
}
```
`@hyperapp/router`を使って、パスによって表示を切り替えしてますね。[React Router](https://reacttraining.com/react-router/)とか使ってる人にはお馴染みの書き方だと思う。

ポイントは`View`の型付けかな。型テンプレートを使ってあって、なんでも入るようになってるんですが、ここに先ほど作った交差型`AppState`と`AppActions`をぶち込んで、それら以外の違うメンバの入ったものが渡されたらエラーにするわけですな。


次は、コンポーネントのルートです。

```ts:src/components/index.ts
import { Home } from './Home'
import { About } from './About'

export const Components = {
  Home,
  About
}
```
受け渡ししてるだけですね。大事な流通業者です。

```tsx:src/components/Home.tsx
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
import { Modules as M } from '../modules'

export const Home = (): any => (state: M.AppState, actions: M.AppActions) => (
  <div>
    <h1>{state.count}</h1>
    <button onclick={() => actions.down(1)}>-</button>
    <button onclick={() => actions.up(1)}>+</button>
    <div>
      <Link to="/about">About</Link>
    </div>
  </div>
)
```

`../modules`を`import`するのがちょっと微妙だと思ってて、ページスペシフィックなstate/actionsはこのファイルに直書きした方が良いかもしれない。一応、counterのstate/actionsはこれから作る別のページでも使うって体で別にした。

JSXノードを返す関数を返す関数って作りになってるけど、これはHyperappの[Lazy Components](https://github.com/jorgebucaran/hyperapp#lazy-components)って機能です。これを使うと、グローバルのステートとアクションが渡されてくるので、バケツリレーをしなくてよくなる。エッジで静的なやつは`props: any`を受け取ってJSXノードを返すだけでいいと思う。

返り値の型を特に指定してないわけですが、あんま変なの送ったらHyperappの方の型定義がちゃんと怒ってくれるという寸法です。楽したいんや。できるなら`any`も省略したいんや。しかし、CLIプログラムやAPI作る時の原則に「入力は寛容に、出力は厳密に」って標語があるんだけど、それとは逆になってるな……。でも、めんどくささが勝った！


これで全部晒したかな？終わり？終わり？

## 結果

開発サーバーで動かした時のDevToolsのAudits(LightHouse)はこんな感じです。

<img width="546" alt="スクリーンショット 2018-11-28 17.24.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/16003/04fe7a02-cccb-13f1-5fc0-359f7cbec405.png">

Best Practicesはhttp2じゃないからってやつで、ホスティング次第ですので、実質100点です！ということに。

PWAはそういう風に作ってないので、また今度[Workbox](https://developers.google.com/web/tools/workbox/)かなんか使ってリポジトリを進めてみましょうかね。

とりあえず、今回はここまで。お疲れ様でした。