{"version":"https://jsonfeed.org/version/1.1","title":"log.pocka.io","home_page_url":"https://log.pocka.io","feed_url":"https://log.pocka.io/feed/ja.json","description":"pocka's personal blog","icon":"https://log.pocka.io/favicon.png","authors":[{"name":"pocka (Shota Fuji)","avatar":"https://en.gravatar.com/userimage/138857166/a98b0dd6d0db54c944732ce2f5d9ab7b.png"}],"language":"ja","items":[{"id":"https://log.pocka.io/posts/ja/js-network-resource-algebraic-data-type/","url":"https://log.pocka.io/posts/ja/js-network-resource-algebraic-data-type/","title":"非同期データを宣言的にView層に持ち込む","summary":"Tagged Unionを使って非同期なデータを宣言的にView層に持ち込む手法の紹介","date_published":"2020-11-20T15:00:00.000Z","date_modified":"2020-11-20T15:00:00.000Z","tags":["article","javascript","typescript","react"],"content_html":"非同期データを宣言的にView層に持ち込む || log.pocka.io

非同期データを宣言的にView層に持ち込む

Last updated at

対象読者

\n
    \n
  • JS/TS でアプリケーションを組める
  • \n
  • 基本的な TypeScript のコードが読める
  • \n
  • 非同期処理を View 層で処理するのに困っている / 今の書き方に満足していない
  • \n
  • 関数型プログラミングって実際使えるの?いいの?って疑問に思っている
  • \n
\n

コードサンプルは基本的に TypeScript で書かれています。一部を除いてわかりやすいようにシンプルに書いています。\nまた、View 層は React で書いていますがライブラリは問いません。ただ TypeScript との相性がいいライブラリほど有用性が高まります。

\n

この記事に書かれているコードはあくまでも実装の一例であり、実際にこの手法を利用する場合は要件や用途に合わせたコードを書いてください。

\n

解決したい問題

\n

SPA が広く普及したことにより、クライアントサイドの複雑さは昔に比べて非常に増えました。特に一番複雑化に寄与しているのが UI と非同期処理の組み合わせです。\n読み込みやエラーといった状態の上にビジネスロジックが乗るせいで、簡潔に書くことが難しく、バグの温床になりやすいのです。

\n

今回は、特に頻度の高い非同期データの UI での表示の複雑さを関数型プログラミング※1の手法を取り入れて解決してみます。

\n

なお、わかりやすくするために非同期データ=ネットワーク越しのデータ(= fetch や XHR で取得するもの)とします。

\n

※1 ... 正しくいうなら関数型プログラミングのものというわけではないが、ちゃんと活用しているのが主に関数型プログラミング界隈なのと関数型の要素との相性が非常にいいため。

\n

複雑さの理由

\n

非同期データが扱いにくいのはその状態の多さです。最小でも以下の 4 パターンの状態が存在します。

\n
    \n
  • 読み込みが開始されていない
  • \n
  • 読込中
  • \n
  • 読み込まれた
  • \n
  • エラー
  • \n
\n

そしてこれをよくある書き方、特に手続き型なスタイルで多い方法で表すなら以下のようになるでしょう。

\n
// サンプルとしてどこかのAPIからユーザを取得する、という想定とします\n// また、以降のサンプルも全て同じ想定とします\nfunction SampleComponent() {\n  // ユーザをAPIからfetchしてくるHooksとします (実装はReact固有なので省略)\n  // コンポーネント表示時に自動的にfetchするHooksとします\n  const userApi = useUserApi()\n\n  // 読み込み未開始は面倒なので省略しています\n  // 以降のサンプルでも全て省略します\n  if (userApi.isLoading) {\n    return <p>読込中です...</p>\n  }\n\n  if (userApi.error) {\n    return <p>エラー: {userApi.error}</p>\n  }\n\n  return <p>名前: {userApi.value.name}</p>\n}\n
\n

最小限しか書いていないのもありますが問題ないように見えます。しかし、"状態" という観点から見るととても複雑なコードとなっています。

\n

変数による状態の管理

\n

それぞれの属性 (=変数、上記コードの場合はuserApi.isLoadinguserApi.error等) で状態を表現しようとすると、プログラムが表現する状態の数が実際に起きうる、もしくは必要な状態数よりもかなり多くなってしまいます。

\n

上記コードを例にすると、起きうる状態は

\n
    \n
  • API から読み込んでいる途中 (LOADING)
  • \n
  • 読み込みに失敗した (FAILED)
  • \n
  • API からユーザ情報を取得した (OK)
  • \n
\n

の 3 つですが、コードで表現される状態は isLoading error value それぞれのとりうる値の組み合わせの総数となります。「状態に応じてそれぞれちゃんと値を設定するでしょ」と思うかもしれませんが、プログラム的に制限されない = 書いている人間に全て任されることになります。人間は間違える生き物なのでプログラム的に起き得ないようにするべきでしょう。

\n

認知コストの増大

\n

とりうる状態を読み取ろうとする際に、結果の属性 (userApiの中のプロパティ) を全て把握する必要があります。特に複数の処理を同時に扱ったりすると把握が非常に難しくなります。人間の脳は簡単にスタックオーバーフローを起こすので、できるだけ避けるべきでしょう。

\n

TypeScript との親和性の悪さ

\n

上記コードをちゃんとした TS で書くなら以下のようになります。

\n
function SampleComponent() {\n  const userApi = useUserApi()\n\n  if (userApi.isLoading) {\n    return <p>読込中です...</p>\n  }\n\n  if (userApi.error) {\n    return <p>エラー: {userApi.error}</p>\n  }\n\n  return <p>名前: {userApi.value!.name}</p>\n}\n
\n

違いは最後のreturn内のuserApi.value!、つまり「nullundefinedじゃないよマーク」がついたことです。これは一般的に書かれるこのパターンの型が以下のようになっているためです。

\n
// useUserApiの返却値の型\ninterface UserApi {\n  isLoading: boolean\n  error: Error | null\n  value: User | null // Userはユーザ情報の型定義とします\n}\n
\n

ロジック上はisLoading === true、もしくはerror === nullでなければvalue === nullにはならないのですが、プログラム的にはそうじゃない場合というのが発生してきます。この Non-Nullish キャストは any 型と同じくできるだけ避けないと型システムによる安全性というものを失ってしまいます。ただ一応、直和型 (Union Type) を使うことで回避することはできますが、使い勝手が悪くなり値を提供する側 (useUserApiの実装) も少し複雑になってしまいます。実際に View 層で扱おうとすると非常に面倒で、結果的に Non-Nullish キャストやanyが湧いてきてしまいます。

\n
type UserApi =\n  | {\n      isLoading: true\n      error: null\n    }\n  | {\n      isLoading: false\n      error: Error\n    }\n  | {\n      isLoading: false\n      error: null\n      value: User\n    }\n
\n

状態を表すデータ型: Tagged Union

\n

この、「フラグや変数による状態の表現」の代わりに関数型言語※2でよく使われているのが Tagged Union(Sum Types, Discriminated Union) というものです。数学が苦手なので詳しい説明はググってもらうとして、雑に言うなら区別ができて、値を持てる型です。Haskell では Algebraic data type (代数的データ型、ADT 厳密には異なる?)、Elm では Custom Type、OCaml や ReasonML では Variant と呼ばれているみたいです。

\n

この Tagged Union というのは「状態の表現」という目的を果たすのに最も適していると言っても過言ではないしょう。例として Elm で上記のサンプルコードを書き直してみます。なるべくシンプルに書いているので Elm を知らない/よくわからないという人でもなんとなくわかるようになっているはずです。

\n
{--\nElmでは本来エラー用の型を使わず、Result型というものを使ってエラーを表現するのですが、\n比較しやすくするために文字列でエラーを保持するようにしています\n--}\ntype UserApi\n  = Loading\n  | Failed String\n  | Ok User\n\n{--\nHooksの代わりにAPIの状態を引数で渡しています\n--}\nsampleComponent : UserApi -> Html Msg\nsampleComponent userApi =\n  case userApi of\n    Loading ->\n      p [] [ text "読込中です..." ]\n    Failed err ->\n      p [] [ text ("エラー:" ++ err )]\n    Ok user ->\n      p [] [ text ("名前:" ++ user.name )]\n
\n

case ~ ofはパターンマッチングと呼ばれるもので、JS で言うならそれぞれの枝が値を返すswitchの式バージョンだと考えて大丈夫です。このサンプルでは状態に応じて返す内容の全体が変わりますが、部分的に変わるような UI でも問題なく書けます。

\n

コードのsampleComponentの部分を見るとわかるかと思いますが、Tagged Union や代数的データ型を使うと以下のメリットがあります。

\n
    \n
  • 状態の数が必要な分だけになる (プログラム的にも認知的にも)
  • \n
  • その状態に関連する値だけを考えればよい\n
      \n
    • サンプルを例にすると、エラー時のエラー内容や読み込み完了時のユーザ情報
    • \n
    • そもそも別の状態にしか存在しない値にアクセスできなくなる
    • \n
    • ex) Loading状態のときにuserの値は取得できない
    • \n
    \n
  • \n
\n

この Tagged Union は React とそれ以降の View ライブラリのほとんどが採用している宣言的レンダリングとの相性が非常に良いのです。React は関数型ベースのデザインになっているので当然といえば当然なんですが。

\n

※2 ... もちろん全部ではない。静的型付けの関数型(寄り)の言語は持っている気がする。あと Rust にような新しい言語にも乗ってたりする。

\n

JS/TS で Tagged Union

\n

JS ではコンベンション、 TypeScript ではちゃんと型定義をすることで、ただのオブジェクトを使って Tagged Union を実現できます。

\n
// TypeScriptがなくても書けますが、TypeScriptを使うことを*強く*推奨します\ntype UserApi =\n  | {\n      state: 'loading'\n    }\n  | {\n      state: 'failed'\n      error: Error\n    }\n  | {\n      state: 'ok'\n      value: User\n    }\n\nfunction sampleComponent() {\n  const userApi: UserApi = useUserApi() // useUserApiがUserApi型を返すものとします\n\n  // if文を使って書くこともできますが、今回は返す内容が部分的ではなく全部異なるのでswitchの方が適しています\n  switch (userApi.state) {\n    case 'loading':\n      return <p>読込中です...</p>\n    case 'failed':\n      return <p>エラー: {userApi.error}</p>\n    case 'ok':\n      // nullからの解放!\n      return <p>名前: {userApi.value.name}</p>\n  }\n}\n
\n

文法が違うことを除けば Elm 版のコードと殆ど一緒です。重要なのは、オブジェクトに識別用のプロパティをもたせることです。このサンプルの場合はstateというプロパティがその対象の状態を表します。TypeScript を使っていれば現在の状態に紐付いていないプロパティにはアクセスできなくなります。

\n
// ...\n\nswitch (userApi.state) {\n  // ...\n  case 'failed':\n    // 以下のuserApi.value.nameの部分がコンパイルエラーになる\n    return (\n      <p>\n        エラー: {userApi.error} (ユーザ={userApi.value.name})\n      </p>\n    )\n  // ...\n}\n
\n

このデザインは意外とよく使われていて、有名なところだと Redux の Actions が採用しています。というか JS 界隈は Redux で広まったのかも...と思っちゃうくらいメインコンセプトとして使われてます。

\n

実際に使うには

\n

非同期データを View 層に渡す度に逐一この型定義を書くのは無駄なので、ジェネリック型にまとめることで使いやすく、見やすくなります。

\n
// progress: number みたいのをつけて進捗表示を出す、といったことも簡単にできます\ninterface Loading {\n  state: 'loading'\n}\n\ninterface Failed<E = Error> {\n  state: 'failed'\n  error: E\n}\n\ninterface Ok<T> {\n  state: 'ok'\n  value: T\n}\n\ntype RemoteResource<T, E = Error> = Loading | Failed<E> | Ok<T>\n
\n

型チェックがあっても文字列比較を都度書きたくない!という場合はヘルパを作るといいでしょう。

\n
function isLoading(r: RemoteResource<any>): r is Loading {\n  return r.state === 'loading'\n}\n\nfunction isFailed<E>(r: RemoteResource<any, E>): r is Failed<E> {\n  return r.state === 'failed'\n}\n\nfunction isOk<T>(r: RemoteResource<T>): r is Ok<T> {\n  return r.state === 'ok'\n}\n
\n

また、関数型でよく使われるだけあって関数でアレコレするのと非常に相性がいいです。

\n
\nサンプル: 複数の非同期データをまとめる関数\n
// 実際のプロジェクトで使っている関数を一部改変\n//\n// 複数の非同期データを一つの非同期データとしてまとめる関数\n// 実際に使われているものは読込中の進捗(%)もいい感じにまとめられる\nconst combineRemoteResources = <C, T extends any[]>(\n  combine: (...args: T) => C,\n  ...resources: { [K in keyof T]: RemoteResource<T[K]> }\n): RemoteResource<C> => {\n  const failed = resources.find(isFailed)\n\n  if (failed) {\n    return failed\n  }\n\n  const loading = resources.find(isLoading)\n\n  if (loading) {\n    return { state: 'loading' }\n  }\n\n  const allOk = resources.every(isOk)\n\n  if (allOk) {\n    return {\n      state: 'ok',\n      value: combine(...((resources as Ok<any>[]).map(r => r.value) as T))\n    }\n  }\n\n  return { state: 'empty' }\n}\n\nconst user: RemoteResource<User>\nconst isJoined: RemoteResource<boolean>\n\n// user=loading && isJoined=ok ... loading\n// user=loading && isJoined=failed ... failed\n// user=ok(ユーザ) && isJoined=ok(false) ... ok(null)\n// user=ok(ユーザ) && isJoined=ok(true) ... ok(ユーザ)\ncombineRemoteResources(\n  (user, isJoined): User | null => (!isJoined ? null : user),\n  user,\n  isJoined\n)\n
\n
\n

実装時の注意

\n

あくまでもこの「非同期処理の状態を Tagged Union で表す」のはロジックと View 層の境界のみにしましょう。JS には Promise/async-await といった非同期に関する扱いやすい標準があるため、ロジック内部等までこれをしてしまうと不揃いになりとても扱いにくくなってしまいます。この手法は宣言的なフローに持ち込むことによって真価を発揮します。なお、fp-ts 等を使ってアプリケーションを全部関数型チックに書くのであればこの限りではありません(そもそもそういう書き方をする人はこんな記事見ないと思いますが)。

\n

おわりに

\n

関数型言語を使ったり、関数型のスタイルに切り替えるのはとても大変です。しかし、プログラミング言語もデザインパターンも所詮は道具、必要なときに必要な分だけ使っても全然問題ないのです。ES2015 以降の JavaScript や TypeScript は関数型言語の血が濃くなってきているので関数型界隈で使われているデザインや手法との相性がいい場面がどんどん増えてきています。関数型言語を学ぶことは実践で使える引き出しを増やすことにもなるので、実際に使う機会がなかったとしても学んでみることをおすすめします。

\n

「この宣言的な感じいいな〜」とか「全部こんな感じで書きたいな〜」とか「言語レベルでのサポートとかパターンマッチングいいな〜」て思った人は以下のものを試してみるときっと幸せになれます。

\n
    \n
  • ReasonML\n
      \n
    • OCaml という関数型言語をベースにした言語。JS にコンパイルできる(というかそれがメインな気が)。
    • \n
    • JS との親和性 (interop, 相互運用性) を重視しているので実際のアプリケーションにガンガン使える。多分。
    • \n
    • Facebook 製なこともあり React との相性がいい。というか JSX がある。
    • \n
    \n
  • \n
  • Elm\n
      \n
    • Haskell ベースの関数型言語。AltJS。
    • \n
    • 文法が非常にシンプルになってる。
    • \n
    • 純粋 = コンポーネントの概念がない = ステートを持てない ので実用的な UI を作る場合は CustomElements とかと併用することになると思う。
    • \n
    • Redux の基になっている。
    • \n
    • JS やブラウザ API との相互運用性はかなり微妙。色々見てると作者はあまり web に聡くないと感じる (それで成り立つくらい抽象化されているとも言える)。
    • \n
    \n
  • \n
  • fp-ts\n
      \n
    • TypeScript で関数型プログラミングをするための型やオブジェクト、関数が詰まったライブラリ。
    • \n
    • 部分的に使うもよし、全部関数型で書いてみるもよし。
    • \n
    • ドキュメントが微妙。
    • \n
    • AltJS と異なり、型の判定とかはオブジェクトとかを使ったランタイムでのタグチェックになる。パフォーマンス第一なとこなんかで使う場合は軽くソースコードを確認してから使うことを推奨。
    • \n
    \n
  • \n
"},{"id":"https://log.pocka.io/posts/ja/using-json-for-select-value/","url":"https://log.pocka.io/posts/ja/using-json-for-select-value/","title":"select要素の値にJSONを使う","summary":"主にSPAを使ってコンポーネントを作る際に役立つTIPS","date_published":"2020-06-26T01:00:00.000Z","date_modified":"2020-06-26T01:00:00.000Z","tags":["blog","html","javascript"],"content_html":"select要素の値にJSONを使う || log.pocka.io

select要素の値にJSONを使う

Last updated at

<select/> は地味に面倒

\n

複数の選択肢の中から一つを選ばせる UI である<select/>は便利なのでよく使われますが、SPA などを使った抽象化レイヤーに慣れていると地味に扱いが面倒です。というのも、<option/>valueは文字列だからオブジェクトをそのまま突っ込んだりできず、オブジェクトの ID やインデックスをとったりシリアライズ処理を書いて選ばれた値と欲しいデータのマッピングをするケースがよくあります。

\n
// Reactの場合\nconst items = [\n  {\n    id: 1,\n    name: 'Foo'\n  },\n  {\n    id: 2,\n    name: 'Bar'\n  }\n]\n\nconst MyComponent = () => {\n  const [selected, setSelected] = useState(null)\n\n  const handleChange = ev => {\n    setSelected(items.find(item => item.id === Number(ev.currentTarget.value)))\n  }\n\n  return (\n    <select value={selected?.id} onChange={handleChange}>\n      {items.map(item => (\n        <option key={item.id} value={String(item.id)}>\n          {item.name}\n        </option>\n      ))}\n    </select>\n  )\n}\n
\n

選択肢がオブジェクトだけの場合は比較的シンプルですが、nullが混じってきたりオブジェクトではなく配列だった場合はさらに面倒になります。

\n

JSON で DX を爆上げする

\n

こういった場合は<select/>のラッパーコンポーネントを用意することが多いですが、その際に※1オススメなのが JSON value 方式です。これは単純に、<option/>valueJSON.stringifyした値を使い、<select/>valueJSON.parseする、というものです。

\n

※1 ... もちろんラッパーを用意しなくても使えます。

\n
// Reactの場合\n\n// MyOption.jsx\nexport const MyOption = ({ children, value }) => {\n  const serialized = useMemo(() => JSON.stringify(value), [value])\n\n  return <option value={serialized}>{children}</option>\n}\n\n// MySelect.jsx\nexport const MySelect = ({ children, value, onChange }) => {\n  const handleChange = ev => {\n    onChange(JSON.parse(ev.currentTarget.value))\n  }\n\n  const serialized = useMemo(() => JSON.stringify(value), [value])\n\n  return (\n    <select value={serialized} onChange={handleChange}>\n      {children}\n    </select>\n  )\n}\n\n// 使い方\nconst Page = () => {\n  const items = [\n    {\n      id: 1,\n      name: 'Foo'\n    },\n    {\n      id: 2,\n      name: 'Bar'\n    }\n  ]\n\n  const MyComponent = () => {\n    const [selected, setSelected] = useState(null)\n\n    return (\n      <MySelect value={selected} onChange={setSelected}>\n        {items.map(item => (\n          <MyOption key={item.id} value={item}>\n            {item.name}\n          </MyOption>\n        ))}\n      </MySelect>\n    )\n  }\n}\n
\n

React の Context のような値の伝播を使うこともできますが、実装が複雑になる上 straight forward な使い方ではなくなってしまいます。JSON value であれば基本的な HTML(JavaScript)の利用方法で使える上、どんなフレームワークやライブラリ(ライブラリを使わなくても)でも使えます。

\n
<!-- Vueの場合(色々省略) -->\n<script>\n  export default {\n    props: {\n      value: {}\n    },\n    computed: {\n      $_value() {\n        return JSON.stringify(this.value)\n      }\n    },\n    methods: {\n      handleChange(ev) {\n        this.$emit('change', JSON.parse(ev.currentTarget.value))\n      }\n    }\n  }\n</script>\n\n<template>\n  <select :value="$_value" @change="handleChange">\n    <slot />\n  </select>\n</template>\n
\n

注意点

\n

当たり前ですが、この方法にもいくつかダウンサイド/注意点があります。

\n
    \n
  • JSON シリアライズ/デシリアライズは比較的コストの高い処理なので、パフォーマンスに気をつける必要がある(キャッシュする等)
  • \n
  • Symbol/ネイティブオブジェクト/関数等のシリアライズできないものや、NaNのように変換されてしまうものは値として利用できない
  • \n
  • オブジェクトのキー順序は担保されない: 常に同じ結果が返ってくるプラットフォームが多いが、一部(例: Go 言語)では並び順は常に同じとは限らない
  • \n
\n

パフォーマンスに関しては適切なキャッシュやメモ化をしていれば特に問題になることはないでしょう。2 つ目に関しても通常の利用であればそこまで問題になることはないですが、頭の隅にでも入れておく必要はあります。最後のものに関しては主に SSR や既にある HTML を JS でいじるときに気をつける必要が出てきます。

"},{"id":"https://log.pocka.io/posts/ja/vue-docgen-loader/","url":"https://log.pocka.io/posts/ja/vue-docgen-loader/","title":"Storybook for VueのDocs Addonの裏側: コンポーネントの自動ドキュメント生成について","summary":"Storybook Docs Addonでどのようにコンポーネントのドキュメントを自動生成しているか、またそのツールを使った独自のドキュメント作成についてなど","date_published":"2020-02-27T09:00:00.000Z","date_modified":"2020-02-27T09:00:00.000Z","tags":["blog","vue.js","webpack","storybook"],"content_html":"Storybook for VueのDocs Addonの裏側: コンポーネントの自動ドキュメント生成について || log.pocka.io

Storybook for VueのDocs Addonの裏側: コンポーネントの自動ドキュメント生成について

Last updated at

はじめに

\n

Storybook の公式アドオンである Addon Docs は、5.3 から Vue(や Angular や Web Components)におけるコンポーネントドキュメントの自動生成機能がサポートされるようになりました。\nこれは、コンポーネントを指定するだけでその Props や Events, Slots の型やコメントがドキュメントとして生成できる、というものです。

\n

この記事では、如何にして Vue 版のドキュメントが自動生成されているのか、またその仕組みを Storybook 以外で使うことについて説明します。

\n

コンポーネントの情報抽出

\n

ドキュメントを自動生成するには、まずどうにかしてコンポーネントの(Props 等)を取得、またそれらに対して書かれたコメントを抽出する必要があります(メタ情報の抽出)。\nこの工程をまるっと行ってくれるのが、Vue Styleguidist※1というプロジェクトのvue-docgen-apiというライブラリです。

\n

vue-docgen-apiはコンポーネントファイルを読み込み、その内容を解析して JSON として出力してくれます。

\n
<!-- Foo.vue -->\n\n<template>\n  <div>\n    <!-- @slot my slot -->\n    <slot />\n  </div>\n</template>\n\n<script>\n  /**\n   * I'm Foo!\n   */\n  export default {\n    props: {\n      /**\n       * Foo the Bar.\n       */\n      foo: {\n        type: String\n      }\n    }\n  }\n</script>\n
\n

こんなコンポーネントから、

\n
{\n  "description": "I'm Foo!",\n  "props": [\n    {\n      "description": "Foo the Bar.",\n      "name": "foo",\n      "type": {\n        "name": "string"\n      }\n    }\n  ],\n  "slots": [\n    {\n      "description": "my slot"\n    }\n  ]\n}\n
\n

こんな感じのメタ情報を生成してくれます。(色々省略しています)

\n

※1 ... Storybook と同じようなコンポーネントカタログツール。どちらかというとドキュメントの部分に重きを置いている。

\n

メタ情報を利用する

\n

vue-docgen-apiによって抽出されたメタ情報はvue-docgen-loaderという webpack のローダーによって、コンポーネントオブジェクトにこっそり追加されます。

\n
import Foo from 'Foo.vue'\n\nconsole.dir(Foo.__docgenInfo) // メタ情報がそのまま詰まっている\n
\n

そして Docs Addon はこのコンポーネントにひっついているメタ情報を元に、Props や Events, Slots のテーブルをレンダリングする、というわけです。

\n

Storybook 以外でメタ情報を使う

\n

前述したようにメタ情報はvue-docgen-apiによって生成されvue-docgen-loaderによってコンポーネントに注入されます。\nつまり、この一連のプロセスには一切 Storybook 関連のものが入っていないということになります。\nそのため、webpack 環境さえあればFoo.__docgenInfoでメタ情報を抽出できる環境を構築することができます。

\n

具体的にどんな用途に使えるのかは人やプロジェクトによって異なりますが、例えばスクラッチでドキュメントを作成する際にコンポーネントのメタ情報を自動生成する、というようなことが可能になります(サンプル)。

\n

注意点

\n

vue-docgen-loaderは名前の通り webpack のローダーなのでやりたい放題できてしまいます。\nしかし、コンパイルにかかる時間やバンドルサイズを考えると、あくまでもドキュメンテーションのようなメインのアプリケーション以外にのみ利用することを推奨します。

"},{"id":"https://log.pocka.io/posts/ja/typescript-promisetype/","url":"https://log.pocka.io/posts/ja/typescript-promisetype/","title":"TypeScriptでPromiseの型パラメータを取り出す","summary":"型パラメータを取り出す簡単な方法","date_published":"2020-01-31T04:00:00.000Z","date_modified":"2020-01-31T04:00:00.000Z","tags":["blog","typescript"],"content_html":"TypeScriptでPromiseの型パラメータを取り出す || log.pocka.io

TypeScriptでPromiseの型パラメータを取り出す

Last updated at

やりたいこと

\n

A = Promise<B>である際にAからBを導き出したい。

\n
type A = Promise<B>\n\ntype C = /* AからBを取り出したい */\n
\n

やりかた

\n

inferを使う。

\n
type PromiseType<T extends Promise<any>> = T extends Promise<infer P>\n  ? P\n  : never\n\ntype A = Promise<B>\n\ntype C = PromiseType<A> // B\n
\n

型名の直後のextends Promise...はなくても動くが、使う際にわかりやすくなるため個人的には書くことを推奨。

\n

ちなみにutility-typesというパッケージがこの型を公開しているので、ユーティリティ型を多用する場合はそちらを使ったほうがよい。

\n

また、Bluebird のような似非 Promise を(or も)使いたい場合はPromiseLikeを使うとよい。

\n
type PromiseType<T extends PromiseLike<any>> = T extends PromiseLike<infer P>\n  ? P\n  : never\n\ntype A = PromiseType<Promise<number>> // number\n\nimport * as Bluebird from 'bluebird'\n\ntype B = PromiseType<Bluebird<string>> // string\n
\n

このinferを使った型パラメータの抽出は書き方を覚えておくと、さくっと書けて結構便利。

\n
interface Box<T> {\n  value: T\n}\n\ntype BoxType<T extends Box<any>> = T extends Box<infer P> ? P : never\n\ntype A = Box<number>\n\ntype B = BoxType<A> // number\n
\n

どんなときに必要か

\n

基本的には外部のライブラリを使う際、特にPromiseを返す関数の結果の型を使いたい場合に役立つ。

\n
// a.ts\nexport const someApi = () => Promise.resolve(0)\n\n// b.ts\nimport { someApi } from 'a.ts'\n\ntype ResponseOfApi = PromiseType<ReturnType<typeof someApi>>\n\nfunction mutateApiResponse(res: ResponseOfApi) {\n  /* ... */\n}\n\nmutateApiResponse(await someApi())\n
"},{"id":"https://log.pocka.io/posts/ja/npm-package-config/","url":"https://log.pocka.io/posts/ja/npm-package-config/","title":"package.jsonのconfigを使おう","date_published":"2019-12-26T06:30:00.000Z","date_modified":"2019-12-26T06:30:00.000Z","tags":["blog","javascript","npm"],"content_html":"package.jsonのconfigを使おう || log.pocka.io

package.jsonのconfigを使おう

Last updated at

package.jsonの中のconfigプロパティ、使っていますか?

\n

configとは

\n

[公式リンク]

\n

JS のプロジェクトを動かす場合にはほぼ必ず作成するpackage.json、その中に設定するプロパティの一つです。\n基本的な用途としては名前の通り、設定を保持することとなります。\n値にはオブジェクトを指定することができ、その中身については自由に指定することができます。

\n
// package.json\n{\n  "name": "foo",\n  "config": {\n    "port": 8080,\n    "dev": {\n      "hmr": false\n    },\n    // 何を書いてもおk\n    "foo": {\n      "bar": ["baz"]\n    }\n  }\n}\n
\n

また、npm configコマンドや.npmrcファイルによってユーザが値を上書きすることができます。

\n

どういう風に使うの?

\n

基本的な使い方としては、package.json#configに設定を書き、利用側は環境変数$npm_package_config_xxx(xxxはプロパティ名)を使って値を取得します。

\n
// package.json\n{\n  "name": "foo",\n  "scripts": {\n    "start": "node ./index.js",\n    "dev": "http-server -p $npm_package_config_dev_port"\n  },\n  "config": {\n    "dev": {\n      "port": 3000\n    }\n  }\n}\n
\n
// index.js\nconst port = process.env.npm_package_config_dev_port;\n\nsomeApp.start(port);\n
\n

npm_package_config_xxxで何故値が取得できるのか?

\n

npm script を実行すると、package.jsonの中身がnpm_package_xxxという形で自動的に展開されます。\n例えば以下のpackage.jsonがある場合:

\n
// package.json\n{\n  "name": "foo",\n  "version": "1.2.3",\n  "config": {\n    "bar": {\n      "baz": 3\n    }\n  }\n}\n
\n

以下のような環境変数がセットされます。

\n
npm_package_name = "foo"\nnpm_package_version = "1.2.3"\nnpm_package_config_bar_baz = 3\n
\n

プロパティアクセサは_になります。($npm_package_config_bar_baz ≒ npm.package.config.bar.baz)

\n

また、configの内容に関してはnpm config.npmrcの内容が優先されるため、ユーザの設定値 || デフォルト設定値という挙動になります。\nなお、const pkg = require('./package.json')としてしまうとユーザの設定値が反映されないため注意が必要です。

\n

何がいいの?

\n

これの優れている点は"標準的な方法で、ユーザが上書き可能な設定値を記載できる"というものです。

\n

一般的にはこういった設定は環境変数で指定することが多いと思いますが、その場合 README に説明を記載したり、利用側で初期値を指定することになります。npm script で直接利用する場合は(特にクロスプラットフォームを意識するなら)簡単には初期値を指定できません。\nまた、クレデンシャルといったセンシティブな情報と設定値が一緒に管理されてしまうというデメリットもあります。

\n

この方式の中で、.envを使って無理やりやっているケースをたまに見ます。どうやっているんだろうとよく見てみると、大量の変数が書き込まれたサンプルをコピーさせたり※1特定の.envファイル(.env.local等)を git 管理に含めるような、アンチパターンのオンパレードとなっていることが殆どです。

\n

configプロパティの場合、使うのは npm と環境変数だけで外部ツールには一切依存しません。\nまた、初期値の上書きはnpm configコマンドや.npmrcの編集でできます※2

\n
// package.json\n{\n  "name": "foo",\n  "scripts": {\n    "dev": "echo $npm_package_config_dev_port"\n  },\n  "config": {\n    "dev": {\n      "port": 3000\n    }\n  }\n}\n
\n
$ npm config set foo:dev.port 8080\n$ npm run dev\n# 8080\n
\n

※1 ... サンプルの値 != 初期値\n※2 ... npm config~/.npmrcをいじるコマンドだからやってることはぶっちゃけ一緒

\n

どうやって上書きすればいいの?

\n

上述したように、基本的にはnpm config set~/.npmrcに値を書き込むのが一番簡単です。

\n

ただ、これはグローバルな設定なのでプロジェクトルートに.npmrcファイルを作成し、そこに値を書き込むのが個人的にはおすすめです。\n.npmrcファイルをプロジェクトで使っていない/すべて Yarn を使うようにしている場合は.npmrcを git 管理から外したうえでプロジェクトルートに.npmrc`を作成して値の上書きをさせるとよいでしょう。

\n

なお、既にプロジェクトで.npmrcを使っている(git 管理対象内)の場合は、個人で上書きした設定がコミットされてしまう可能性があるため、グローバル設定に書き込むことをおすすめします。

\n

具体的にどんなものを書けばいい?

\n

以下のようなものが適しています。基本的に開発時に使うものが殆どになると思います。

\n
    \n
  • 開発サーバのポートやホスト名、バインドするインターフェイス名等(ネットワーク周り)
  • \n
  • ファイル監視(watch)の有効/無効化
  • \n
  • コンパイル先ディレクトリの指定
  • \n
  • etc...
  • \n
\n

また、webpack.config.jsの中でnpm_package_config_xxxを参照して〜といったことも可能です。(npm scripts で動く前提...普通はそうするはずなので)

\n

ただ、npm scripts は(yarn も)コマンドライン引数をコマンドに渡せるため、内容がシンプルかつプログラムの初期値を利用する/他で初期値を指定できる場合は、コマンドライン引数を使ったほうがいいです。

\n
// package.json\n{\n  "name": "foo",\n  "scripts": {\n    // npm run dev --port 3030 / yarn dev --port 3030\n    "dev": "webpack-dev-server",\n    // こういうのだとcpコマンドにしか引数を渡せない\n    // tscに上書き可能な設定を渡したいならconfigを使ったほうがいい\n    "build": "tsc && cp ./docs ./dist/"\n  }\n}\n
\n

デメリットは?

\n

正しく利用する(センシティブな情報を含めない等)分には基本的にはないです。

\n

しかし、環境変数名が滅茶苦茶長くなるので npm script に書く場合はある程度気をつけたほうがいいかもしれません。

"},{"id":"https://log.pocka.io/posts/ja/cloud-development-irl/","url":"https://log.pocka.io/posts/ja/cloud-development-irl/","title":"クラウド開発環境 - 実際の使用感","date_published":"2019-11-28T05:00:00.000Z","date_modified":"2019-11-28T05:00:00.000Z","tags":["blog","development"],"content_html":"クラウド開発環境 - 実際の使用感 || log.pocka.io

クラウド開発環境 - 実際の使用感

Last updated at

info

この記事は最終更新から1年以上経っています。情報が古くなっている可能性があります。

はじめに

\n

手元の端末からクラウド上の開発サーバに接続して開発を行うクラウド開発。導入方法や周辺ツールの紹介は多々(?)ありますが、実際に使ってみてどうなのか、という情報があまりない気がするのでそこら辺について書いてみたいと思います。

\n

思ったことをそのまま書き連ねただけなので、文章がわりと適当ですがどうかご容赦を。

\n

利用用途/ツール等

\n

主にフロントエンド開発に用いています。アプリや Electron 開発は行っていません。\nエンド向けのアプリケーション以外にライブラリや開発用ツール等の開発も行っています。

\n

Google Compute Engine のインスタンス(4 vCPUs, 4.75GB memory)を使っており、SSH 接続には GCP のコンソール(管理画面)から起動できるブラウザターミナルを利用しています。テキストエディタは基本的にcode-server(VSCode の OSS 版を web クライアント-サーバ形式にしたもの)で、英語での簡単な編集であれば vim を使っています。\nインスタンスへの SSH 以外の接続は VPN 経由で接続しており、専用のドメインにクラウド側の内部 IP を振っています。

\n

サーバ上の OS は Debian で、クライアントは ChromeOS(Pixelbook)ときどき Windows って感じです。

\n

ローカル開発と比較した際のデメリット

\n

多くの人が先に気になるであろうデメリットから書いていきます。

\n\n

低品質ネットワーク環境下でのラグ

\n

あrぐがおおkるおtけっこうつあり。

\n

よくやり玉に挙げられるこれ、機会としてはぶっちゃけそんなにありません。VDSL+WiFi(5GHz)でもラグを感じることは殆どありません。帯域幅よりも応答速度が肝心(なはず)なので、家庭用ネットワークではまずストレスを感じることはないでしょう。

\n

問題は低品質なネットワークにあたってしまったときです。ほんの少しのラグであったとしても、それが普段行っている行動に生じたものである場合、人間はそれを過剰に感じ取ります。しかもそれがテキスト編集、つまり開発の基本アクションであった場合は最悪です。そのストレスは、足で 7/8-5/4 拍子を刻みながら利き手じゃない手でイライラ棒をするレベルに匹敵します。

\n

対策としては以下のようなものが考えられます。個人的には 2 番をよく採用しています。

\n
    \n
  • ネットワークを変える(物理的な場所移動も含む)
  • \n
  • どうにかなるまで開発しない
  • \n
  • そこそこのストレス耐性を会得する
  • \n
\n

まぁ、そこまで遭遇するものでもないので個人的には"そこまでは"気にならないです。慣れたり忘れたりしてるだけかもしれませんが...。

\n

使えるツール(特にエディタ)が限られる

\n

ちょっとした縛りプレイです。

\n

ChromeOS や iPadOS 等で開発をしなければそこまでの制約はありませんが、それでも基本 SSH 越しに操作をするので多少の制限はあります。

\n

特にエディタに関してはクライアント-サーバ方式のものがまだ少ないのである程度のリサーチと選別が必要になってきます。ただまあ、現状多くの人が利用しているであろう VSCode がクライアント-サーバ方式で動くので、大抵のケースではさほど問題にならないとも言えます。

\n

クラウドの利用料がかかる

\n

お金がかかります。

\n

自分の環境はコスト最適化をしていないのもあって、平均して毎日 8 時間くらいの利用で 4000 円近くかかっています。無駄に確保したストレージ周りをすっきりさせても 3000 円くらいはかかるかと思います。

\n

個人的にはあまりデメリットとして捉えてはいませんが、人によっては、というか大抵の人にはデメリット(downside)となるでしょう。

\n

いいこと/メリット

\n

個人的に感じているメリットの中で、特に恩恵を受けているなーというものに絞ってみました。

\n\n

プラットフォームの呪縛から解放される

\n

ソフトウェア的にもハードウェア的にも。

\n

まともなブラウザさえ動けばマシン/OS はどれでもよくなるため、好きなマシンを選ぶことができます。別のマシンに乗り換えたり、複数台で使う場合でも接続情報さえあれば完全に同じ環境で作業することができます。

\n

これは実感してみないとあまりメリットと感じられないことだと思います。個人的に感じているのは「いつマシンが壊れても大丈夫」「どんなマシンでも大丈夫」という安心感が一番大きいです。選択肢が広がるメリットというよりは精神衛生的なメリットといえるでしょうか。

\n

マシンの負荷が下がる

\n

サクサクです。

\n

開発サーバや Language Server といった開発で使う重いプログラムが全てクラウド上で動くため、ローカルマシンの負荷が下がる、というかほぼ消え去ります。マシンに必要なのはエディタのフロントエンドと確認用のブラウザプロセス(web 開発の場合)を動かすリソースくらいです。

\n

スペックの高いマシンなら変わんないじゃん、と思う人もいるかもせれません。超絶変わります。具体的には発熱とバッテリー持ちが別人のように改善します。

\n

ファンが回らずメーカー公称値通りのバッテリー駆動時間で開発マシンが動くというのは想像以上に快適です。

\n

クリーンな Linux 環境で開発できる

\n

ピュアピュアです。

\n

クラウド上に構えるサーバは基本的に Linux で、GUI を用いないことが多いため余計なものが殆ど入っていません。デスクトップ Linux やラップトップ Linux を使ったことのある人ならわかると思いますが、Linux での GUI とかやろうとすると相当に messy になります。そういった汚い部分が一切ない、いうなれば「開発用 Linux に欲しいとこだけまとめた」環境が自然に手に入ります。

\n

開発で使うコマンドラインツールが動かない/エラーになったり、環境依存の問題に当たることは滅多にありません。

\n

総合的な満足度は?

\n

10 out of 10.

\n

今まで何年もかけて Linux や ChromiumOS、Windows 等を色々渡り歩きながら最適な開発環境を追い求めてきて、ようやく最適解に辿り着いたという感じです。

\n

コストやらネットワークの問題やらはありますが、これらはクラウド界隈の発展や技術レベルの向上によって解決できる問題なので、個人的には大した問題ではないと考えています。

\n

ランニングコストがかかったり、初期の構築にある程度手間と知識が必要だったりするので、正直万人にオススメできるものではないです。しかし、今よりも進んだ未来の中の一つの形ではあると思います。興味がある人は試してみてこの麻薬のような快適さを味わってみてはどうでしょうか。

"},{"id":"https://log.pocka.io/posts/ja/motiv-ring-review/","url":"https://log.pocka.io/posts/ja/motiv-ring-review/","title":"「Motiv Ring」 レビュー","date_published":"2019-10-23T06:00:00.000Z","date_modified":"2019-10-24T06:00:00.000Z","tags":["article","review","wearable"],"content_html":"「Motiv Ring」 レビュー || log.pocka.io

「Motiv Ring」 レビュー

Last updated at

info

この記事は最終更新から1年以上経っています。情報が古くなっている可能性があります。

おすすめ度: ★★★★☆

\n

製品情報

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
名前カテゴリ公式ページ
Motiv Ring指輪型ウェアラブルデバイスhttps://apac.mymotiv.com/ (日本向けページ、英語)
\n

どんなもの?

\n

Motiv Ring は装着者の心拍数/歩数/睡眠時間などを計測してくれる指輪型のウェアラブルデバイス。ウェアラブルデバイスというとやけに大きく不格好であったり、デバイス感溢れる見た目だったりするが、このデバイスは普通の指輪と同じようなサイズ/見た目になっているのが特徴。

\n\n \n \n \n \n\n

見た目/装着感

\n

見た目としてはちょっと厚めな指輪、といった感じ。故障期間を除くと大体 9 ヶ月ほど使っているが、一度もウェアラブルデバイスと気づかれたことはない。

\n\n \n \n\n

LED インジケータがついており、同期中や充電中等に光ることがあるが、頻度は低く地味なためあまり気にならない。

\n

つけ心地は非常に快適。非常に軽く、つけていることを忘れるレベル。

\n

素材は外側がチタンで内側はプラスチック?のようなもの(どこにも記載がなかったが、爪で叩いた感じはプラっぽい)。外側の部分は 3 色から選ぶことができる。ただ、塗装しているだけのようでわりと傷が付きやすい。個人的にはあまり気にならないが、Amazon.com のレビューに傷についてのものが結構あるので購入する場合は事前に確認しておくことをおすすめする(画像もあり)。自分の場合はハンドグリッパーを握った際に視認できる傷(というか跡)がついた。日常生活で使うだけならばヘアライン程度にしかつかないので大抵の人は気にならないと思う。

\n

計測について

\n

歩数以外の計測値については概ねウェアラブルとしては普通、といったところ。

\n

歩数は指につけていることもあり、カウントが明らかにおかしい。一日中部屋にいるのに歩数 500 となっていたり、実際の歩数の 2 倍近くカウントされていたりする。歩数は全く参考にならないと思っておいたほうがいい。

\n

心拍数計測は当時のデバイスとしては珍しく、数分間隔で計測してくれる。計測値は Mi Band 2/3, Galaxy Watch Active 等と大体同じような値なので、精度としては問題はない。心拍数が高くなると計測間隔が短くなるので、運動時は詳細に計測、平常時はある程度の間隔で計測してくれるようになっている。

\n

睡眠トラッキングはそこそこ正確にとれている。ただ、物理的に起き上がらないと睡眠状態としてカウントされたりもする。ここらへんは他のデバイスでも同じだったりするのであまりマイナスポイントではないかな。

\n

計測したそれぞれの値は専用のアプリで閲覧することができる。心拍数と歩数に関しては連携させることにより Google Fit と同期させることも可能。睡眠に関しては追加予定となっているが一向に追加される気配がない...。

\n

バッテリー

\n

バッテリーは体感 1.5~2 日くらい持つ。

\n

充電は専用の充電用機器がついているので、USB 端子にそれを挿して充電端子部分にリングを引っ掛ける形。充電端子部分はマグネットになっており、接触が悪く充電できない、といったことはあまりない。

\n

充電速度は速くもなく遅くもなく、といったところ。毎日シャワーを浴びる際に充電する、といったように毎日定期的に充電していればバッテリーが切れることはない。

\n

アプリ/ファームウェア(ソフト面)

\n

基本的にスマートフォンのアプリを使い、Bluetooth 経由で接続、計測値を閲覧したりファームウェアのアップデートを行ったりする。アプリは iOS/Android 両方とも用意されているが、Android 版は公式サポートデバイスが非常に少なく(Pixel シリーズと Galaxy シリーズのみ)、それ以外の端末は動くかもしれないけど公式にはサポートしていないよ、というスタンス。

\n

アプリの出来は...正直微妙。今はある程度ましになったが、以前はかなり酷かった。現在でもちょくちょくペアリングが解除されたような表示になったり(アプリ再起動で治る)、Unpaired になったりする。頻度は低くなったが...。UI 等の使い勝手は良いとはいえないが、他のウェアラブル用アプリと比べると、シンプルで割とマシな方ではある。

\n

\"アプリ画面\"

\n

ちなみに、アプリを完全に終了してしまうとペアリングが切れてしまうようなので、設定等でずっと立ち上げっぱなしにする必要がある(バックグラウンドプロセスを終了させない)。

\n

ファームウェアも正直怪しく、同期できなくなったり、アップデートできずに起動しなくなったりしたこともあった。ただ、こちらに関してはアップデートで割とマシになった。現在はまあ使えるだろう、というレベル。

\n

全体的にソフトウェア周りのクオリティが残念。

\n

購入/アフターケア等

\n

購入は Motiv の公式サイト、もしくは Amazon(.com の方)から可能。

\n

購入フローは少し変わっていて、

\n
    \n
  1. サイズ確認用リングを購入
  2. \n
  3. サイズを確認後、色とサイズを選んで指輪を注文
  4. \n
\n

というようになっている。

\n

サイズ確認用リングにディスカウントコードがついているので、本体分の値段だけで買えるようになっているが、関税や送料がかかるので、知り合いに持ってる人がいるならばそれを借りたほうがよい。

\n

サポートはメールで、記載がないのでおそらく英語のみと思われる。が、国際発送しておりサポート担当の人たちも非常に親切なため、高校レベルの英語や Google 翻訳で十分やりとりはできると思う。ちなみに、サンフランシスコ時間(UTC-07:00、東京から-16 時間)なのでメールの返信はどうしても遅くなってしまう。

\n

不満があるとすれば、故障して交換してもらった際、受け取り時に関税を払う必要があったことくらい...。サポートのクオリティはかなり高い方だと思う。

\n

おすすめ度

\n

★★★★☆

\n

装着感、見た目ともにこのクラスのものは他に存在しないので、特に「ウェアラブルはつけたいけど見た目がちょっと...」というような人におすすめできる製品。スポーツ等でとにかく正確な数値が欲しい人は専用のデバイスを買ったほうがいいが、普通に使うには十分なので、見た目を気にしたり手首に何かつけるのが嫌な人にはとても良いのではないだろうか。

\n

ソフト面がマイナスポイントだが、アップデートによって段々と安定してきているので、サポートとの簡単な英語でのやり取りに抵抗がないなら是非確認してみることをおすすめする。

\n

おすすめできる人

\n
    \n
  • ウェアラブルつけたいけど、普通のデバイスはダサい!って人
  • \n
  • 手首に変なの巻きたくない人
  • \n
  • おしゃれにも気を使いたい人
  • \n
\n

おすすめできない人

\n
    \n
  • とにかく正確な数値が欲しい人
  • \n
  • 通知機能とかも欲しい、かつ全て一つのデバイスで済ませたい人
  • \n
  • 指輪つけたくない人
  • \n
\n
\n

個人的な感想としては「めっちゃいいぜこれ!」

"},{"id":"https://log.pocka.io/posts/ja/react-context-api-tabindex/","url":"https://log.pocka.io/posts/ja/react-context-api-tabindex/","title":"ReactのContextを使ったtabindex制御","summary":"ReactアプリケーションでContext APIを使い柔軟かつ直感的にtabindexのON/OFFを制御する方法について説明します","date_published":"2019-09-20T09:00:00.000Z","date_modified":"2019-09-20T09:00:00.000Z","tags":["blog","javascript","react","a11y"],"content_html":"ReactのContextを使ったtabindex制御 || log.pocka.io

ReactのContextを使ったtabindex制御

Last updated at

info

この記事は最終更新から1年以上経っています。情報が古くなっている可能性があります。

はじめに

\n

Web のアクセシビリティの話でまず出てくるtabindex属性、皆さんちゃんと使っていますでしょうか。\n特にスタイルに凝っていない、普通の web サイトを作る場合は特に指定しなくても勝手にいいかんじに動いてくれる※1ので、もしかしたら使ったことがない人もいるかもしれません。

\n

この属性は Web アクセシビリティ的には非常に重要となります。\nしかし、特に SPA を実装する場合なんかに割と無視されがち※2なうえ、単純だけどちゃんと設定しようと思うと地味に面倒なんですよね。

\n

この記事では React を使ったアプリケーションにおいて、簡単、直感的かつ柔軟にtabindexを管理する方法について説明します。

\n

なお、inertを使う場合(要polyfill)はこの記事の内容は必要なくなります。

\n

※1 ... :focusのスタイルを外したりしない前提\n※2 ... 筆者の体感です

\n

想定読者

\n
    \n
  • React を使ったアプリケーションを書ける
  • \n
  • React の Context API についてある程度理解している
  • \n
  • tabindex属性がどんなものか知っている(tabindex at MDN)
  • \n
\n

TL;DR

\n
    \n
  • TabNavigatableContext(boolean)のようなコンテキストを作成する
  • \n
  • タブによるフォーカス可能な要素はそのコンテキストの値を見てtabindex0-1を設定する
  • \n
  • あるコンポーネント以下の要素全てにフォーカスを巡らせたくない場合は<TabNavigatableContext.Provider value={false}>で要素を包む
  • \n
  • モーダルがある場合は Provider 内で参照 or トップレベルコンポーネントで制御
  • \n
\n
const Button = () => {\n  const isTabNavigatable = useContext(TabNavigatableContext)\n\n  return <button tabIndex={isTabNavigatable ? 0 : -1}>BUTTON</button>\n}\n\nconst App = () => {\n  const [modalVisible, setModalVisible] = useState(false)\n\n  return (\n    <TabNavigatableContext.Provider value={!modalVisible}>\n      <TabNavigatableContext.Provider value={modalVisible}>\n        <Modal visible={modalVisible}>\n          <Button />\n        </Modal>\n      </TabNavigatableContext.Provider>\n      <Button />\n    </TabNavigatableContext.Provider>\n  )\n}\n
\n

タブナビゲーション可能かどうかのコンテキスト

\n

このテクニックの中心は、「コンテキスト内の要素のフォーカス可/不可」を提供するコンテキストです。

\n
// コンテキスト外で使った際にフォーカスが当たらなくなってしまうのを防ぐため\n// 初期値はフォーカスが当たるようにしている\nconst TabNavigatableContext = createContext(true)\n\n// コンテキストのプロバイダ\nconst TabNavigatableProvider = ({ children, navigatable = true }) => {\n  return (\n    <TabNavigatableContext.Provider value={navigatable}>\n      {children}\n    </TabNavigatableContext.Provider>\n  )\n}\n
\n

このコンテキストは navigatableの値を伝播するだけのものとなります。\nそして、フォーカスが当たる可能性のある要素全てでこのコンテキストの値を参照します。

\n
const MyButton = props => {\n  // navigatable: boolean\n  const navigatable = useContext(TabNavigatableContext)\n\n  return <button tabIndex={navigatable ? 0 : -1} {...props} />\n}\n
\n

あとは普通にコンポーネントを使うだけです。\nもし、子要素にフォーカスを当てたくないコンポーネントがある場合は、TabNavigatableProviderで子要素を優しく包み込んであげましょう。

\n
const WhimsicalUI = ({ children }) => {\n  const navigatable = useContext(TabNavigatableContext)\n\n  const [visible, setVisible] = useState(true)\n\n  return (\n    <div\n      style={{\n        opacity: visible ? 1 : 0,\n        pointerEvents: visible ? 'all' : 'none'\n      }}\n    >\n      <TabNavigatableProvider navigatable={visible}>\n        {children}\n      </TabNavigatableProvider>\n    </div>\n  )\n}\n
\n

モーダル表示中にモーダル外にフォーカスを移動させない

\n

モーダルダイアログ UI では、ダイアログの外にフォーカスを回さないようにするのが一般的です(トラップ)。<dialog>要素を使うことで簡単に実現できますが、ブラウザ対応状況が芳しくないため、まだまだ実用は難しい状況です。

\n

しかし、このコンテキストを使えば、はためんどくさいトラップの実装が簡単にできてしまいます。

\n
// モーダルが開いているかどうかの状態を保持するだけのコンテキスト\n// トップレベルでモーダルを管理していたり、別の機構がある場合は必要なくなる場合も\nconst ModalContext = useContext({\n  exists: false,\n  open: () => 0,\n  close: () => 0\n})\n\nconst ModalProvider = ({ children }) => {\n  const [count, setCount] = useState(0)\n\n  const value = useMemo(\n    () => ({\n      exists: count > 0,\n      open() {\n        setCount(prev => prev + 1)\n      },\n      close() {\n        setCount(prev => prev - 1)\n      }\n    }),\n    [count]\n  )\n\n  return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>\n}\n\n// コンテキストのプロバイダにもちょっと手を加える\nconst TabNavigatableProvider = ({\n  children,\n  navigatable = true,\n  force = false\n}) => {\n  const modal = useContext(ModalContext)\n\n  const value = (force || !modal.exists) && navigatable\n\n  return (\n    <TabNavigatableContext.Provider value={value}>\n      {children}\n    </TabNavigatableContext.Provider>\n  )\n}\n
\n

あとは、モーダル UI となるコンポーネントでModalContextを操作すれば完成です。

\n
const Modal = ({ open, children }) => {\n  const modal = useContext(ModalContext)\n\n  useEffect(() => {\n    if (!open) {\n      return\n    }\n\n    // 開く際にコンテキストのメソッドを呼ぶ\n    modal.open()\n\n    // 閉じる際にもコンテキストのメソッドを呼ぶ\n    return modal.close\n  }, [open])\n\n  return (\n    <div className="backdrop">\n      <div className="container">\n        {/* forceがないとnavigatableの値が無視されてしまう */}\n        <TabNavigatableProvider navigatable={open} force>\n          {children}\n        </TabNavigatableProvider>\n      </div>\n    </div>\n  )\n}\n
\n

モーダルをどのように実装するかによって詳細な実装は変わってきますが、基本的に<TabNavigatableProvider>の中でモーダルが存在しているかどうかを知り、存在していれば値を強制的にfalseにするだけです。

\n

DOM を走査してフォーカス可能な要素を取得してtabindexを変えて...とやるよりは、宣言的に書くことができるためバグも入りにくく、予期せぬ挙動が起きにくくなります。

\n

おわりに

\n

このテクニックを使えば、子要素をプロバイダで囲うだけで簡単かつ柔軟にtabindexの制御を行うことができます。また、プロバイダの中にさらに条件等を追加することも可能です。

\n

しかし、簡単になっても「いかに開発者がアクセシビリティを意識できるか」が重要であることには変わりません。tabindexはアクセシビリティ対応としてだけではなく、一般のユーザに向けた操作性向上にもなるのでしっかりと意識して実装していきましょう。

"},{"id":"https://log.pocka.io/posts/ja/typescript-builtin-type-functions/","url":"https://log.pocka.io/posts/ja/typescript-builtin-type-functions/","title":"TypeScript特有の組み込み型関数","summary":"TypeScript特有のジェネリックな組み込み型関数の一覧と説明、及び使用例","date_published":"2018-07-22T15:00:00.000Z","date_modified":"2019-09-20T05:30:00.000Z","tags":["article","typescript"],"content_html":"TypeScript特有の組み込み型関数 || log.pocka.io

TypeScript特有の組み込み型関数

Last updated at

info

この記事は最終更新から1年以上経っています。情報が古くなっている可能性があります。

TypeScript には Promise や Symbol といった Javascript 特有のグローバルオブジェクト以外に、型を扱う上で便利になるような組み込みのジェネリックな型関数※1が存在します。これらは非常に便利で様々なプロジェクトで使われているので~すが、公式にリストもなく、説明も主にリリースノート等にしかないため、~使い方等を交えて説明を書いていきたいと思います。

\n

note

\n

公式のドキュメントページ(英語)ができたようです。いくつか載っていないものもありますが、一先ずそちらを参照することをおすすめします。

\n

\n

なお、各定義はMicrosoft/TypeScript のsrc/lib/es5.d.tsにあります

\n

※1 ... 型を受け取って新しい型を返す型。多分正しい呼び名ではない。

\n

Partial

\n\n
type Partial<T>\n
\n

型 T のすべてのプロパティを省略可能(つまり| undefined)にした新しい型を返す Mapped Type です。

\n
interface Foo {\n  bar: number;\n  baz: boolean;\n}\n\ntype PartialFoo = Partial<Foo>;\n\n// PartialFoo {\n//   bar?: number\n//   baz?: boolean\n// }\n
\n

ライブラリのオプションの型を書く時なんかに非常に役立ちます。

\n
interface Options {\n  host: string;\n  port: number;\n}\n\nconst defaultOptions: Options = {\n  host: "localhost",\n  port: 3000,\n};\n\nconst main = (options: Partial<Options> = {}) => {\n  const opts = { ...defaultOptions, ...options };\n\n  // ...\n};\n\n// 使う側\n\nmain({\n  port: 8080,\n});\n
\n

Required

\n\n
type Required<T>\n
\n

型 T の全てのプロパティを必須にした新しい型を返します。

\n
interface Foo {\n  bar?: number;\n  baz: boolean;\n}\n\ntype RequiredFoo = Required<Foo>;\n\n// RequiredFoo {\n//   bar: number\n//   baz: boolean\n// }\n
\n

Readonly

\n\n
type Readonly<T>\n
\n

型 T の全てのプロパティにreadonly属性をつけた新しい型を返します。

\n
interface Foo {\n  bar: number;\n  baz: boolean;\n}\n\ntype ReadonlyFoo = Readonly<Foo>;\n\n// ReadonlyFoo {\n//   readonly bar: number\n//   readonly baz: boolean\n// }\n
\n

Pick

\n\n
type Pick<T, K extends keyof T>\n
\n

型 T の中から K に当てはまるプロパティのみを抜き取った新しい型を返します。\nなお、K には型 T に存在するキーを指定する必要があります。

\n
interface Foo {\n  bar: number;\n  baz: boolean;\n  qux: string;\n}\n\ntype BarAndQux = Pick<Foo, "bar" | "qux">;\n\n// BarAndQux {\n//   bar: number\n//   qux: string\n// }\n
\n

react-redux 等の HoC を作成/使用する際に多用します。

\n

Omit

\n\n
type Omit<T, K extends keyof any>\n
\n

型 T の中から、キー名が K に当てはまるプロパティを除外した新しい型を返します。\nなお、Pick とは異なり K には T のキー名以外を指定することができます。

\n
interface Foo {\n  foo: string;\n  bar: number;\n  baz: boolean;\n}\n\ntype FooWithoutBar = Omit<Foo, "bar">;\n\n// FooWithoutBar {\n//   foo: string\n//   baz: boolean\n// }\n
\n

Record

\n\n
type Record<K extends keyof any, T>\n
\n

型 T なプロパティ K を持つレコード型を作成します。

\n
interface Foo {\n  bar: number;\n  baz: boolean;\n}\n\ntype StringFoo = Record<keyof Foo, string>;\n\n// StringFoo {\n//   bar: string\n//   baz: string\n// }\n
\n

Exclude

\n\n
type Exclude<T, U>\n
\n

型 T が型 U に代入可能であればnever、そうでなければ型 T を返す Conditional Type です。\n主に Union Types から特定の型を取り除く際に使われます。

\n
type A = Exclude<string, number>; // string\ntype B = Exclude<string, any>; // never\ntype C = Exclude<string | number | boolean, string | boolean>; // number\n
\n

Extract

\n\n
type Extract<T, U>\n
\n

型 T が型 U に代入可能であれば型 T、そうでなければneverを返す Conditional Type です。\nExclude の逆ですね。\n主に Union Types から特定の型を抽出する際に使われます。

\n
type A = Extract<string, number>; // never\ntype B = Extract<string, any>; // string\ntype C = Extract<string | number | boolean, string | boolean>; // string | boolean\n
\n

NonNullable

\n\n
type NonNullable<T>\n
\n

型 T からnullundefinedを取り除いた型を返します。

\n
type A = NonNullable<string | null>; // string\ntype B = NonNullable<null>; // never\ntype C = NonNullable<string>; // string\n
\n

Parameters

\n
    \n
  • 利用可能バージョン: TypeScript3.1~
  • \n
  • リリースノート及びドキュメントに記載なし
  • \n
\n
type Parameters<T extends (...args: any[]) => any>\n
\n

関数型 T の引数の型をタプルとして抽出します。

\n
function foo(arg1: string, arg2: number): void {}\nfunction bar(): void {}\n\ntype Foo = Parameters<typeof foo>; // [string, number]\ntype Bar = Parameters<typeof bar>; // []\n
\n

ちょくちょく使える場面はあるので覚えておくと便利です。

\n

ConstructorParameters

\n
    \n
  • 利用可能バージョン: TypeScript3.1~
  • \n
  • リリースノート及びドキュメントに記載なし
  • \n
\n
type ConstructorParameters<T extends new (...args: any[]) => any>\n
\n

型 T のコンストラクタの引数の型をタプルとして抽出します。\nParametersのコンストラクタ版です。

\n
class Foo {\n  constructor(arg1: string, arg2?: boolean) {}\n}\n\ntype Bar = ConstructorParameters<typeof Foo>; // [string, boolean] | [string]\n
\n

ReturnType

\n\n
type ReturnType<T extends (...args: any[]) => any>\n
\n

型 T の戻り値の型を返します。なお、T には関数型のみ指定可能です。

\n
const foo = () => "foo";\n\ntype A = ReturnType<typeof foo>; // string\ntype B = ReturnType<typeof window.setTimeout>; // number\ntype C = ReturnType<() => void>; // void\n
\n

InstanceType

\n\n
type InstanceType<T extends new (...args:any[]) => any>\n
\n

型 T のコンストラクタの返り値の型を返す型です。

\n
class Foo {}\n\ntype A = InstanceType<typeof Foo>; // Foo\ntype B = InstanceType<any>; // any\ntype C = InstanceType<never>; // any\n
\n

使いどころが全くわからない...。

\n

ThisType

\n
    \n
  • 利用可能バージョン: TypeScript2.3~
  • \n
  • リリースノート及びドキュメントに記載なし
  • \n
\n
type ThisType<T>\n
\n

this の型を T とすることができる特殊な型です。型関数ではないですが便利なので一応記載します。

\n
interface Foo {\n  bar: number;\n}\ninterface Baz {\n  qux(): string;\n}\n\nconst quux: ThisType<Foo> = {\n  myMethod() {\n    return this.bar;\n  },\n};\n\nconst corge: Baz & ThisType<Foo> = {\n  qux() {\n    return this.bar.toString(16);\n  },\n};\n
\n

Javascript のライブラリを使う際にあると便利な型です。\n詳細は Qiita に記事を上げてくれている人がいるので、それを参照してください。

\n

TypeScript 2.3 RC 変更点@vvakame