アクアリウムを始めます(マイナス3日目)

突然ですがアクアリウムを始めます。始めるのは突然ですが思いたったのは7年〜6年前に駒場に住んでた時から。家の斜め前にアクアリウムショップがあってよく通りかかるうちに興味をもち、カタログもらったり、雑誌買ったりして妄想膨らませていました。

f:id:masataka_k:20191212093445j:plain

当時わからなかったけど今わかるのは、そのアクアリウムショップはアクアデザインアマノ(ADA)の特約店だったのですね。もらったカタログも買った雑誌も全部ADAの出版物だった。雑誌の解説の通りにモノを揃えてくと結構な金額になるなーと思いながらためらってるうちに渡米することとなりました。そこで一旦はアクアリウム熱もおさまり忘れてました。

今回のきっかけは妻からです。家から徒歩1分ぐらいのところにアクアリウムショップがあって(やはりADAの特約店だった)、ウチのゴールデンレトリバー牡4歳が毎朝の散歩の途中でショップ店長に撫でてもらうのが日課になってました。撫でてもらってる最中に雑談でアクアリム知識を仕入れてきて、妻がアクアリムを始めたくなっちゃった。駒場の時には子育て忙しいわ私は仕事だと言い張って家に帰らねーわで落ち着いて何かする考えでなかったのが、娘達も大きくなって犬も飼って、私も家庭的になって。そうなると「いや、俺はずっとやりたかったし」と私も主張して雑誌も引っ張り出してきて。今後はお父さん水槽でノウハウを蓄積した上でお母さん水槽やお姉ちゃんコケリウム、さらには妹ヒョウモントカゲモドキゲージが現れることになりそうです。

よってこのブログは、しばらくアクアリウム多めでTypeScriptやITビジネス少なめな構成になります。最近手元で面白いライブラリを書いていてTypeScriptやReactもお知らせしたいテクニックがたまってきているのだけど、脳の大半がアクアリムになっちゃってるのだからしょうがない。

TypeScriptのプロジェクトでTSLintからESLintへ移行する

昨年夏ぐらいから、TSLintは終わらせてESLintでTypeScriptプロジェクトも対応していこう、という世界的な流れになってました。「脱TSLint」とか「ESLintでTypeScript」とかググればその辺の記事が出てきます。当時早速私も乗っとこうとESLintに移行しようとしたのですが、対応レシピが複雑で未成熟なことからすぐに諦めて延期していました。今回、手元で新しいプロジェクトを始めるにあたってESLintを試してみたのですがスッキリいい感じ。機は熟した、ESLintでOK。

ESLintとTypeScriptのセットアップ

{
    "scripts": {
        "lint": "eslint src --ext .ts,.tsx"
    },
    "eslintConfig": {
        "extends": ["airbnb", "plugin:@typescript-eslint/recommended"],
        "plugins": ["@typescript-eslint"],
        "parser": "@typescript-eslint/parser",
    },
    "devDependencies": {
        "@typescript-eslint/eslint-plugin": "^2.10.0",
        "@typescript-eslint/parser": "^2.10.0",
        "eslint": "^6.7.2",
        "eslint-config-airbnb": "^18.0.1",
        "eslint-plugin-import": "^2.18.2",
        "eslint-plugin-jsx-a11y": "^6.2.3",
        "eslint-plugin-react": "^7.17.0",
        "eslint-plugin-react-hooks": "^2.3.0",
        "typescript": "^3.7.2"
    }
}

devDependenciesにある一連のものをインストールしますが、前提としてはTypeScriptでReactアプリを書いてます。基本は厳しいことで無条件に信奉しているAirBnBにTypeScriptの手当を最低限だけ。プロジェクトはJSの混入無し前提。全てTSとTSXで書いてます。

ここから先は settings と rules をこまめに調整しています。

 Unable to resolve path to module 対策

まず必要だったのはimport文が「Unable to resolve path to module」と全滅なところ。これは無設定ではJSしかimport先を追いかけないためで、以下を追加しています。

{
    "eslintConfig": {
        "settings": {
            "import/resolver": {
                "node": {
                    "extensions": [".js", ".json", ".ts", ".tsx"]
                }
            }
        }
    }
}

これは、みたまま通りimport文でソースファイルの拡張子が省略されているところを補完する候補を増やすもの。TSとTSXが必要で、その上でnode_modulesの下にインストールされているものを読み込むためにJSが必要。あとはJsonModuleを有効にしている際にはJSONも加えます。よってこのようになりました。

Expected a line break after this opening brace 対策

以下のようなコードで問題があったのを記述で解決します。

<Button icon={props.icon} onClick={() => props.dispatch(props.action)}>OK</Button>

// Must use destructuring props assignmenteslint(react/destructuring-assignment)

Reactのプロパティを展開する際に、props.* のように使ってしまうと react/destructuring-assignment に引っかかる。ならばと、以下のように書き直します。

const { icon, dispatch, action } = props;

<Button icon={icon} onClick={() => dispatch(action)}>OK</Button>

これで「Must use destructuring props assignmenteslint」とは言われなくなりますが、今度は「Expected a line break after this opening brace」と言われるようになる。プロパティをオブジェクトに展開しているところで、1行で書いちゃだめだと。。。改行入れればいいだけなのですが、どうにも縦長になってしまうので object-curly-newline はオフにしました。該当箇所は以下の通り。

{
    "eslintConfig": {
        "rules": {
            "object-curly-newline": ["off"]
        }
    }
}

TSXへの対応

無設定では、JSXタグが、拡張子JSXのファイルでだけ許すという設定になっています。しかし私はTSXオンリーなことにしましたので、以下の手当。

{
    "eslintConfig": {
        "rules": {
            "react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }]
        }
    }
}

一般的には、[".jsx, ".tsx"]とJSXも残しておくのでしょうね。私には不要でしたが。

Reactコンポーネントプロパティの型問題

JSで書く際には型が無いので、Reactコンポーネントのプロパティについては prop-types ライブラリを用いていちいちプロパティとして受け入れる値の定義を示します。しかしTypeScriptで書くとこれがいらない。コンポーネントプロパティについてがっちりTypeScript型システムでガードされるのでAirBnBは許さずとも、私は検出をオフにします。

{
    "eslintConfig": {
        "rules": {
            "react/prop-types": ["off"]
        }
    }
}

devDependencies問題

私は、テストやビルドに用いるための外部ライブラリは全て devDependencies に置くようにしてますが、テストで用いるEnzymeやStoryBook関連のライブラリは手当しないとdevDependenciesではなくdependenciesに置くよう「'enzyme' should be listed in the project's dependencies, not devDependencies」などと言われます。テストとStoryBookはパスパターンで除外しておきます。

{
    "eslintConfig": {
        "rules": {
            "import/no-extraneous-dependencies": ["error", {
                "devDependencies": ["**/*.test.ts*", "src/stories/**/*"]
            }]
        }
    }
}

私は上記だけにしましたが、あとは **/*.spec.ts* も必要でしょうかね。末尾 .ts* としているのは、テストにTSだけでなく、Enzymeで書くTSXがあるからです。正規表現使えないのでこうなりました。。。と、もしかして正規表現書けるのかな?わからず。

書き癖で直したく無いもの

{
    "eslintConfig": {
        "rules": {
            "@typescript-eslint/explicit-function-return-type": ["off"],
            "react/jsx-props-no-spreading": ["off"]
        }
    }
}

色々考えましたが、@typescript-eslint/explicit-function-return-type と react/jsx-props-no-spreading は自分の書き癖でも直したくなかったのでOFF。

// イベントハンドラでも関数戻値の型を書く?次の行では「:void」をあえて書いてみましたが…
<Button icon={icon} onClick={(): void => dispatch(action)}>OK</Button>

// 高階関数や、カリー化した関数では戻値の型を書くのスペース的に難しい
const combineReducer = <S, P>(reducers: ReducerFactory<S, P>[]) => (initialState: S) => {
    const combineded = R.reduce<Reducer<S, P>, Reducer<S, P>>(
        (previous, current) => (state, action) => current(previous(state, action), action),
        (state) => state,
        R.map((r) => r(initialState), reducers),
    );
    return React.useReducer(combineded, initialState);
};

まず、@typescript-eslint/explicit-function-return-type の方ですが上記の2つの例のようにイベントハンドラ高階関数・カリー化した関数では戻値の型を書くのがスペース的に美しく無い。またこれ以外にも関数戻値をTypeScriptの型推論エンジンに任せなければならない稀有な場合もある。よって戻値は型推論エンジンにおか任せするのが吉と思います。おかしなコード書いたらちゃんとトランスパイル通らないし。

<Component {...state} />

// もしくは
<Component {...{ prop1, prop2, prop3, prop4, prop5 }} />

Reactコンポーネントにたくさんの属性引き渡さないといけない時に、どうしてもこういうオブジェクトスプレッド記法で書きたいです。そうしないとここに5個も10個も書かねばならない時もある。上記例での後の方の書き方だけ許すようなルールのオプションもあるのですが、どのみちめんどくさいので、 react/jsx-props-no-spreading はオフ。

グローバルオブジェクトの対応

{
    "eslintConfig": {
        "env": {
            "browser": true,
            "jest": true
        }
    }
}

宣言無しに用いるグローバルオブジェクトでLintエラーを出さないために、env設定を行います。私の場合はJestを用いることでdescribeやitなど、domを直接触るようなコードを書く場合には、windowやdocumentのために、それぞれbrowser: trueやjest:trueを設定します。

Fragmentの書き方

AirBnBのデフォルトでは、React.Fragmentの書き方が省略記法を推奨されてます。

<>{ /*ReactElementの配列など*/ }</>

// 以下はできない
<key={key}>{ /*ReactElementの配列など*/ }</>

これはさっぱりした見かけで私は不慣れながら書いてりゃ見慣れるのですけど、keyを設定したりができない。

<React.Fragment key={key}>{ /*ReactElementの配列など*/ }</React.Fragment>

省略せずにFragmentを書く方が良いかな。react/jsx-fragments で element をオプション指定。

{
        "rules": {
            "react/jsx-fragments": ["error", "element"]
        }
    }
}

結果

もともとTSLintでAirBnB用いて書いてたから、書き癖を重んじてもルールもいじるところ少なくてスムーズに導入できました。ESLintへの移行部分としてはプラグイン設定するだけなので難しいことがない。自分ルールとしてインデントは全部スペース4つと決めています。

完成は以下の通り。ただしESLintに関係するところだけです。

{
    "scripts": {
        "lint": "eslint src --ext .ts,.tsx"
    },
    "eslintConfig": {
        "extends": ["airbnb", "plugin:@typescript-eslint/recommended"],
        "plugins": ["@typescript-eslint"],
        "parser": "@typescript-eslint/parser",
        "env": {
            "browser": true,
            "jest": true
        },
        "settings": {
            "import/resolver": {
                "node": {
                    "extensions": [".js", ".json", ".ts", ".tsx"]
                }
            }
        },
        "rules": {
            "@typescript-eslint/explicit-function-return-type": ["off"],
            "import/no-extraneous-dependencies": ["error", {
                "devDependencies": ["**/*.test.ts*", "src/stories/**/*"]
            }],
            "indent": ["error",4],
            "object-curly-newline": ["off"],
            "react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }],
            "react/jsx-fragments": ["error", "element"],
            "react/jsx-indent": ["error", 4],
            "react/jsx-indent-props": ["error", 4],
            "react/jsx-props-no-spreading": ["off"],
            "react/prop-types": ["off"]
        }
    },
    "devDependencies": {
        "@typescript-eslint/eslint-plugin": "^2.10.0",
        "@typescript-eslint/parser": "^2.10.0",
        "eslint": "^6.7.2",
        "eslint-config-airbnb": "^18.0.1",
        "eslint-plugin-import": "^2.18.2",
        "eslint-plugin-jsx-a11y": "^6.2.3",
        "eslint-plugin-react": "^7.17.0",
        "eslint-plugin-react-hooks": "^2.3.0",
        "typescript": "^3.7.2"
    }
}

unstated-nextとredux-actionsを組み合わせると素敵

github.com

unstated-nextは、Reactの新しいContext APIの極薄のラッパー。ソースコードもたった38行(空行込み)。

github.com

redux-actionsは、FLUXスタイルのアクションを作る極薄ライブラリ。名前にReduxが入ってるのでRedux専用かというと、そうではない。元々Reduxも普通のJavaScript(/TypeScript)関数を組み合わせる仕組みなのでその便利ライブラリたるredux-actionsも普通の関数を作る。

これら二つは、React Hookの一つReact.useReducerで組み合わせる。

ja.reactjs.org

組み合わせると、こんな感じのコードができました。

import React from 'react';
import { createContainer } from 'unstated-next';
import { ActionFunctions, createAction, handleAction, Reducer } from 'redux-actions';
import * as R from 'ramda';

import { ConcreteState } from './types';

type State = ConcreteState;

interface ReducerFactory<Payload> {
    (initialState: State): Reducer<State, Payload>;
}

// あまり美しくない。単にカリー化したいだけなのにR.curry(handleAction)(a, r)ではPayloadの型を失う。
const createReducer = <Payload = void>(
    actionType: ActionFunctions<Payload>,
    reducer: Reducer<State, Payload>,
): ReducerFactory<Payload> => (initialState: State) => handleAction(
    actionType,
    reducer,
    initialState,
);

const useContainer = (reducers: ReducerFactory<any>[]) => (initialState?: State) => {
    if (!initialState) {
        throw new Error('"initialState" is undefined.');
    }
    // 前にやったことと同じ see: http://mk.hatenablog.com/entry/2017/09/09/070852
    const combineded = R.reduce<Reducer<State, any>, Reducer<State, any>>(
        (pre, cur) => (state, action) => cur(pre(state, action), action),
        state => state,
        R.map(r => r(initialState), reducers), // ReducerFactoryの配列からReducerの配列を作る
    );
    // [ State, React.Dispach<Action<any>> ] というTupleを返す。これをアプリから利用する。
    return React.useReducer(combineded, initialState);
}

// ——— Here we go! ———

// redux-actionsでFLUXスタイルのActionを作る
const doSomethingAction = createAction<number>('DO_SOMETHING');
const doSomethingReducer = createReducer(
    doSomethingAction,
    (state, { payload }) => {
         // (具体的な操作は省略) payloadがしっかり型付けできている。
        return state;
    },
);

const doAnotherAction = createAction<boolean>('DO_ANOTHER');
const doAnotherReducer = createReducer(
    doAnotherAction,
    (state, { payload }) => {
        // (具体的な操作は省略) Ramdaを活用してスマートにstateを処理する
        return state;
    },
);

// importが楽なようにActionをまとめた
export const actions = {
    doSomethingAction,
    doAnotherAction,
}

// unstated-nextのReactコンポーネントファクトリでコンテナを作る
export default createContainer(useContainer([
    doSomethingReducer,
    doAnotherReducer,
    // あとはパターンを利用して量産したReducerFactoryを追加していく。
]));

すごく素敵。さらにStateをいじるのにこれまではImmutable.jsとかunderscore.jsを使ってたけど、ここ一年ちょっとぐらいはめちゃ関数型プログラミングないいやつ愛用中。作りがImmutable.jsのように副作用ないのは同じながらImmutable.jsがコレクションを提供していたのでアプリケーション丸ごと染まるのに対して、underscoreのようなコレクション操作等での提供なので、使い始めたいところだけ部分でも使える。ライブラリ全てがカリー化に対応している。

github.com

ここまで全て関数型プログラミングなライブラリ群で、当然ReactもHook使ったFunctionConponentでゴリ押しすると美しい。React.useEffectがあるのでライフサイクル必要でもClassComponentを使う必要なくなった。

そして、UIの見てくれ的なものはこれまでも今もMaterial-UIが良かったのだけど、ここ数日は違うのに浮気中。

github.com

Semantic-UIはCSSライブラリで、JSXのclassName属性へ直書きするだけで十分に使える。さらにSemantic-UI-ReactというReactコンポーネントも用意されていてそれはそれで便利。ただしMaterial-UIのようながっつりコンポーネントで作り込んでるのではないのでCSSの範囲の効果にほぼ限る。複雑なものはMaterial-UIのソース見てだいたい仕組み把握したら、Semantic-UI-Reactのコンポーネントを組み合わせながらSemantic-UIのCSSを使えば結構簡単に良い感じのものができる。

Denoプラグインが普通のTypeScriptプロジェクトに干渉する

VSCodeにDenoプラグインをインストールすると、普通のTypeScriptのプロジェクトでlibの解決ができなくなったりするなど、干渉していました。だいたい問題ないのですけど、たまたまブラウザ環境のグローバル変数の型が解決できなくて困ってました。

const selection = window.getSelection();

こんなのとか、

(node as HTMLElement).appendChild(div);

こんなのでエラー。

前者はtsconfig.jsonにlib: ["dom"]を加えるように示唆までしてくれますが、当然そんなことはやってるって。後者はHTMLElementを@types/reactの方に見に行き、その定義が空っぽなのでアウト。ググっても解決方法見つからなく困ってたところで、独立したまっさらなVSCode環境作って確かめたら問題なく動いたので、やっと気がつきました。プラグインが悪いのだと。そしてそいつはDenoプラグインだと。

プラグインワークスペース単位で無効にできるので、それですぐに解決。Denoはたまに書くためにこのプラグイン自体は大変有用なので残します。

f:id:masataka_k:20191101203617p:plain

このプラグイン詳細画面によると、はっきりと"This extension works using VS Code's built-in version of TypeScript."って書いてありました。いくらtsconfig.jsonにlibを設定しても反映しないはずですね(しかし前のエントリで紹介したtypeRootsやemitDecoratorMetadataの設定は効いてたのでまるっきり見てない訳でもなく、単純な問題じゃなさそう。typeRootsはDenoプラグインを外せば記述の必要もなくなった)。また、Denoプラグインダウンロード回数がたった2016回しかない中で、同じVSCodeで普通のTypeScriptを利用したlibdom.d.tsを参照してもらわないと困るようなプロジェクトをやらない限りぶつからない問題なのでググっても出てこないわけだ。少なくともこの2016回のうち2回は私だし。

vscode typescript tsconfig.json lib dom window document global ts2584 ... この辺のキーワードでググっても出てこないです。今調べたら deno plugin conflict あたりでも全く出てこなかった。世界初のハマりかもしれない。

NestJSの設定で漏れるとダメなやつ

つまらないところでハマったので、未来の私のために備忘。NestJSCLIを使わずに手でプロジェクトを作ったとき、tsconfig.jsonに以下をいれないとダメ。

// tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}
  • experimentalDecorators をいれないと、未来で仕様変わるかも?と警告が出てトランスパイル止められる。これは絶対気がつくので問題なし。
  • emitDecoratorMetadata をいれないと、一見したところトランスパイルも実行も問題なく動くように見えるけど、@Injectableが解決されずサイレントにundefinedで全滅。NestJSのIoTには実行時にTypeScriptの型を保持するというこのオプションが必要。NestJSのガイドの通りにreflect-metadataパッケージを追加 していたけど、emitDecoratorMetadataはガイドに書いてなかった。

VSCodeでJest

最近はどうしてもコード書く機会が少ないので、JetBrains製品のサブスクリプションを維持するのを諦めてこれまで食わず嫌いだったVSCodeに移行したのですが、改めてコード書き始めるとちょいちょいハマったのでメモっておかねば。for 未来の私。

課題

TypeScript + Jestが今までの作法で動かなかった。

解決

必要パッケージを追加する。本体に必要なパッケージ群は別に、Jestで必要なものたちは...

$ yarn add typescript jest ts-jest @types/jest

http://mk.hatenablog.com/archive/2019/05/17

おなじみts-jestはpreset一発で細々としたjest設定が終わり超楽。ドキュメントによるとプリセットは混在する.jsの処理の違いで複数用意されている。テストコードがTypeScriptオンリーなら値は"ts-jest"。そういえばこれ、前にやりました

// package.jsonの関係あるとこだけ抜粋
{
    "jest": {
        "preset": "ts-jest"
    },
    "devDependencies": {
        "@types/jest": "^24.0.19",
        "jest": "^24.9.0",
        "ts-jest": "^24.1.0",
        "typescript": "^3.6.3"
    }
}

ここで問題発生。VSCodeのIntellisenseが期待通りに動作せず、グローバルスコープの、describe・it・jestの名前解決ができない。WEBを調べるとプロジェクトルートにjsconfig.jsonを書けとか見つけたので、VSCodeのドキュメントの該当ページにあたって設定してみたりするも改善しない。

その後にtsconfig.jsonを試行錯誤する中で、以下が当たりだった。 "typeRoots": [ "./node_modules/@types" ] で型ファイルを明示的に取り込んでる。非グローバルはimportでコード上に明示されるのでそもそもに解決できるけど、暗黙となってしまうグローバルの型はここで探索する。これまで経験してきたJetBrains製品(CLion、WebStorm、GoLand)では、"types": [ "jest", "node" ] で読み込んでたのだけど、現在の私のVSCode環境ではそれが上手くなく、 ”typeRoots" にプロジェクトローカルなパス設定となった。もちろん "types" でも方法はあるのだと思うが、こちらの方が堅い上に万一の追加パッケージに柔軟なように考えるのでいいかな。

さらに "types"では、アプリケーション本体に必要なパッケージ群で複数取り込まれていた @types/node で重複エラーが出るという謎動作が出た。パッケージ識別名で捉えるのではなくパスで誤解なく指示するのは、こちらの解決にも効いてる。

// tsconfig.jsonの現在
{
    "compilerOptions": {
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "lib": [ "es6", "esnext" ],
        "module": "commonjs",
        "target": "es5",
        "sourceMap": true,
        "strict": true,
        "typeRoots": [ "./node_modules/@types" ]
    },
    "exclude": [ "node_modules" ]
}

ちなみにlambdaアプリケーションをwebpackでビルドしているので上記その他の設定です。

追記 2019/11/1

これ、Denoプラグインのせいだった。プラグイン外せばtypeRootsではなくこれまでのtypesを利用する設定でもOK。

VSCodeでdeno

愛用するGoLandのサブスクリプションが切れてしまいました。すでにGoLand乗り換えの際にWebStormは捨ててたので、JetBrainsのIDEは無くなった。継続すればいい話なのですが、最近は違う種類の仕事で時間取られて開発作業に関わることが少なく、あってもAtomで仕様書いたりSketchでワイヤー引いたりする程度だったので維持するのをやめました。

そんな中でdenoのプラグインVSCodeにあると本家サイトに書いてあったのを見て、VSCodeにしようかなと思います。退路を断つためAtomはアンインストール。VSCodeを入れて、チュートリアルサイトの学習動画を見て、会社にあったSoftware Design 4月号のVSCode特集をナナメ読み中。

f:id:masataka_k:20190727131558p:plain

プラグインで、deno対応を入れたら、ちゃんとdeno独特なimportを解決してくれました。node上のTypeScriptと異なるのはここだけなのでもう問題ない。デバッガは...どうなるのかな。テスト書けばいいとはいえブレイクポイント置いてインクリメンタルデバッグもできたら最高なのだが。