lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

2022/01-04振り返り

1-4月

生活など

これまでの社会人生活で初めてしっかり体調を崩す😿

きっかけは1月ごろでした。

業務時間中に突然の強烈な頭痛と、胸周りがキュッとなる謎の症状が平日の仕事時間に始まりました。その日の夜はフラフラしながら帰宅した記憶があります・・

その後数日間、いつも通りに仕事をしていましたがやはりあまり症状が回復せず。

特に辛かったのが、これまで愛飲していたコーヒーを飲むと強烈な鈍い頭痛が襲うようになり、「これは本当にまずいな」と思って病院に行くことを決意しました。

内科・脳神経外科MRIなどの精密検査を全て行いましたが、特に不調は見当たらず。内科の先生曰く「仕事のストレスが原因っぽい」とのこと。あと運動をしろ、とのこと。

その診断を受けた頃から土日にコードを読んだり書いたりすることは辞めて、外を散歩したりジムに行くようにしました🙄

結果、少しずつ症状が回復し、1ヶ月程度で完全に回復しました。 コーヒーも問題なく飲めるように。

今振り返ると、運動不足と長時間のデスクワークによる自律神経の乱れが原因だったな、と思います。特に回復に寄与したのがジムでの運動だと思っていて、体を動かすことの重要度を再認識しました。。🥶

今も最低では週2回以上はジムに行ってますが、やはり定期的に通うようになってから体調がいい&デスクワークで頭痛が起きなくなりました😤

かつ、夜ベッドに入るとすぐ寝れるようになった気もします。あとちょっと痩せた。Training Solves All Problems...

仕事

仕事で担当していた重めのPJが終わりに向かう

職場で自分が主担当をしていた新規PJが、テストを含めて一通り終わり無事リリース準備がほぼ終わりました😸
開発期間は去年から続いて約半年ほど?だと思います。

自分のこれまでのキャリアを振り返ってもデベロッパーとして取り組んだ中では最も開発量・難易度ともに高かったPJだったので、とても成長できたような気がします。個人としてもチームとしても反省点はもちろんあったとは思うのですが、とにかく終わってよかったです。

入社直後の自分にこのPJをアサインしていただけたことに本当に感謝しなければならない・・🙇🏻‍♂️過去の自分と比較してですが、めちゃくちゃ成長できました。。

アウトプット・勉強とか

コワーキングスペースの契約

以下の記事でも書きましたが、コワーキングスペースを契約しました。まぁ捗ります。本当にもっと早く契約しておけばよかった。

lamechang-dev.hatenablog.com

ブログ

11記事執筆することができました。基本的には勉強したことやトラブルに直面した時にそれを調査・整理してまとめたり、キャリアのポエムっぽいことを書いたりしてました。

特に閲覧数など多いわけではないし、閲覧数向上を狙った施策などを考えたりなど一切せず好きなことを書いているスタンスを貫いています。。

が、ブログ経由でMeetyの面談依頼が来たり、ブログの記事などを通して副業のお誘いをいただいたりと自分としてはとても嬉しいことがありました😸

5月以降は、本業以外の時間の比重を副業多めで割いていくことになるので、ブログの頻度も月1-2とかになってしまうとは思うのですが、引き続き書いていこうと思います。

ポートフォリオサイト

空き時間を見つけて作っているものがあります。まぁ大したものではないのですが、勉強した技術を試したりする目的で触ったりもしています👍

ある程度雛形はできて、一旦公開できるところまでコードも書いたのでそろそろ公開したい(迫真)。

読んだ本や動画

列挙。

www.amazon.co.jp

競技プログラミングを始めたいと思って買いましたが、一旦業務での利用頻度が少ないのでかなりゆっくり読み進めている状態です。この記事を書いているGWで、これ続きを読んでいる感じです。少しずつ競プロ始めていきたい所存。

www.amazon.co.jp

TypeScriptに関して、今年の前半は「困った時にググる」スタイルでの開発を続けていたのですが、「一度網羅的に説明がある名著を読んだ方が捗りそう」と思って購入しました。一通り読みましたが明らかに解像度が上がったのでおすすめです。また、この本を通したインプットのおかげで、よほどイレギュラーケースでない限り、実務でTSを書いている際に困ることは無くなった印象です。

www.amazon.co.jp

エンジニアのための自己啓発本です😤 キャリアアップのための思考法や、恋愛・投資にまで踏み込んで説明してます。人生マニュアル感。

私のお気に入りはマーケティングの章で、私はこの本に影響を受けてブログを始めたところもあります。定期的に読み返したくなる内容です。おすすめ。

www.udemy.com

React周りのテストについて、より体型的なインプットをしたいと思った&久しぶりに英語の動画で勉強をしたくなってこの動画を現在見ています。が、結構知っていることが多いかも。。。まぁ英語の勉強と思えば。。笑

Udemyといえば私の思い出は2年半プログラミングを始めた時に見たAndrew MeadのJS基礎の動画です。久しぶりに見てなんかエモくなりました。

www.udemy.com

5-8月の目標

  • 体調を崩さず、副業・本業の両立を頑張る
  • ジム通いを継続
  • React周りのテストの勉強を引き続き&アウトプット
  • CI/CD周りの勉強を引き続き&アウトプット
  • アルゴリズム競技プログラミング周りの勉強を引き続き&アウトプット

コワーキングスペースの土日プランを契約した話

コワーキングスペースを契約しました

家から自転車で10分弱程度のところにあるBIZcomfortを土日プランで契約しました。めちゃくちゃいいのでそれについてざっと書きます。

bizcomfort.jp

契約した理由3点

1.副業を再開するため

また本業と別のスタートアップのお手伝いを少しずつ再開することになったので、ガッと集中できる場所が欲しかった&安定したネットワーク環境が欲しかった、というのが一番の理由です。

まぁ、これがなくてもいつか契約したいとは思っていたのですが副業を再開することが背中を押しました😤

これまでは土日は午前中の時間を作業・勉強や副業に充てるようにしていたのですが、最近通っていた喫茶店(タリーズ)のWifiがめちゃくちゃ遅くなったのもあり(ちなみに私は土日に家で勉強や仕事は遊んでしまうので一切できません)、Google meetなどの会議が動画付きでろくにできないことに気づいて喫茶店での作業はもう諦めました。

2.感染対策

なんだかんだカフェは飲食店&みなさんめちゃくちゃ会話しているので、ちょっと飲食店に長居が怖いのもありました😤 コワーキングはみなさんマスクしていて静かですね。

3.めちゃくちゃコスパがいい

BIZcomfortについてですが、土日プランのコスパが本当に良すぎます。これはHPで料金見ていただければわかると思うのですが本当に良心的な値段です。

しかも自分が利用している拠点はなんとディスプレイついてて本当に助かります😿 どこもついてるのかな。あと最近個人事業主として開業したのもあって、経費にできることも考慮すると本当に安い。

また通うに際してちょっと遠いくらいの方が、通うのに軽い運動になっていいかな、と思ったのも契約した理由です。

終わりに

半年ほどは利用するのは間違いないので、またその際にレビューなどざっと書きます。

【TypeScript】レコード型とマップ型

はじめに

今回はTypeScriptにおいてObjectの型定義をしていく上で必須知識とも言っても過言ではないレコード型・マップ型についてです。花粉辛い。

レコード型

TypeScriptの組み込みのRecord 型は 、 あるものからあるものへのマップ(対応付け)としてオブジェクトを表現するための方法です。オブジェクトが特定のキー&バリューの集まりであることを強制するために Record 型は有用です。

以下のような記法を取ります。

type RockPaperScissors = "Rock" | "Paper" | "Scissors";

type Hoge = Record<RockPaperScissors, RockPaperScissors>;

const winTarget: Hoge = {
  Rock: "Scissors",
  Paper: "Rock",
  Scissors: "Paper",
};

類似の記法に インデックスシグネチャ があります。こちらも同様にオブジェクトの値の型を制約できますが、キーに採用できるのは stringnumber だけになりますね。例えば、上記の例で定義した 文字列リテラルのユニオン型である RockPaperScissors は キーに採用できません。エラーも、「代わりにmapped object type を使ってくれ」と言ってます。

このように、Record型のキーは、stringnumberのサブタイプであれば自由に制約をつけることができます。

// An index signature parameter type cannot be a union type. 
// Consider using a mapped object type instead.
const winTarget: { [key: RockPaperScissors]: RockPaperScissors } = {
  Rock: "Scissors",
  Paper: "Rock",
  Scissors: "Paper",
};

また、オブジェクトが特定のキーの集まりを定義することを強制できることもポイントです。以下のように、最初に提示した Record型の例からキーRock を消してみました。するとどうなるでしょうか?

// Property 'Scissors' is missing in type '{ Rock: "Scissors"; Paper: "Rock"; }' 
// but required in type 'Hoge'.ts(2741)
const winTarget: Hoge = {
  Rock: "Scissors",
  Paper: "Rock",
  // Scissors: "Paper",
};

有益なエラーメッセージを得られていることがわかります。これはTypeScriptにおける 完全性による網羅チェックによる挙動になります(この記事では割愛)。

マップ型

マップ型はより強力な(柔軟な)型宣言が可能になります。と言うより、そもそもTypeScriptのRecord型自体がこのマップ型を利用して実装されているため、当たり前といえば当たり前かもしれません。以下がRecord型の実際の定義です。

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Record型の定義に登場した独自の構文がありますね。[P in K]: T記述された部分です。名前が示すように、これはオブジェクトのキー・値両方の型をマッピングするための方法です。具体的な記法の例を以下に紹介していきます。

type Pokemon = {
  skills: Array<string>;
  name: string;
  weekTypes: Array<string>;
};

// すべてのフィールドを省略可能にします type OptionalPokemon = {
type OptionalPokemon = { [K in keyof Pokemon]?: Pokemon[K] };
// すべてのフィールドをnull許容にします type NullablePokemon = {
type NullablePokemon = { [K in keyof Pokemon]: Pokemon[K] | null };
// すべてのフィールドを読み取り専用にします type ReadonlyPokemon = {
type ReadonlyPokemon = { readonly [K in keyof Pokemon]: Pokemon[K] };

このように柔軟に型情報を定義することが可能です。

組み込みのマップ型

上記に紹介したようなマップ型は実際の開発現場でも利用頻度が高いものが多いです。TypeScriptはこのような頻出と思われるマップ型の型定義があらかじめ定義されています。特にチーム開発ではこれらを採用していくことでObjectの型定義の可読性が上がると思うので、積極的に使っていきましょう。

// Keys 型のキーと Values 型の値を持つオブジェクト。
Record<Keys, Values>
// Object内のすべてのフィールドを省略可能と指定します。
Partial<Object> 
// Object内のすべてのフィールドを必須(省略不可)と指定します。
Required<Object> 
// Object内のすべてのフィールドを読み取り専用と指定します。
Readonly<Object> 
// 指定された Keysだけを持つ、Objectのスーパータイプを返します。
Pick<Object, Keys>

終わり

【TypeScript】過剰プロパティチェックの挙動

過剰なプロパティチェック

TypeScriptには、過剰なプロパティチェックを行う機能が存在します。まず、以下の例を見てみましょう。

type Pokemon = {
  ability?: Array<string>;
  weekType: string;
};

const zenigame: Pokemon = {
  // Type '{ abiility: string[]; weekType: string; }' is not assignable to type 'Pokemon'.
  // Object literal may only specify known properties, but 'abiility' does not exist in type 'Pokemon'.Did you mean to write 'ability'?
  abiility: ["bodyAttack", "waterGun"],
  weekType: "denki",
};

オブジェクト zenigameは型 Pokemonを期待しているので、前回の記事の通り、そのサブタイプを渡してあげればよさそうです。

ここで、私はzenigame のプロパティ名を1つ間違えてしまいました。abiility はタイポしてますね。javascriptではこういったバグよく起こりますよね。

これは型エラーになるかなぁと一瞬思うも、しかし、よく見ると型Pokemonのプロパティabilityは型が Array<string> | undefinedであるので、プロパティabilityzenigameにないことは abilityundefinedであることと同値であり、これはArray<string> | undefinedのサブタイプなので条件を満たします。

なので、タイポはしたものの zenigameはサブタイプの条件を満たしているように見えますね。なぜTypeScriptはこのバグを検知することができたのでしょうか?

この挙動こそ、過剰プロパティチェックです。オブジェクトリテラルからTypeScriptが推論を行う型に関して適用されます。フレッシュなオブジェクトリテラル型 Aを別の型 Bに割り当てる際に、Bには存在していないプロパティがAに存在している場合、TypeScript はエラーを吐きます。

仮にそのオブジェクトリテラルが型アサーションを使用しているか、変数に割り当てられているのであればこの挙動は適用されません。具体的に以下の例を見てみましょう。

過剰プロパティチェックが適用されないケース

変数に割り当てられる際の例

type Pokemon = {
  ability?: Array<string>;
  weekType: string;
};

// オブジェクトリテラル型は代入を通して通常のオブジェクト型に拡大される
const zenigame = {
  abiility: ["bodyAttack", "waterGun"],
  weekType: "denki",
};

// OK
const pokemonA: Pokemon = zenigame;

アサーションの例

type Pokemon = {
  ability?: Array<string>;
  weekType: string;
};

// OK 
const zenigame: Pokemon = {
  abiility: ["bodyAttack", "waterGun"],
  weekType: "denki",
} as Pokemon; // asによる型アサーション。オブジェクトの型がPokemonであることをTypeScriptに主張する

と言った感じで、フレッシュでないオブジェクトリテラル については過剰プロパティチェックの挙動は生まれません。

終わり。

コードを一切書かない上流SEを3年やった後にコードを書くWebエンジニアを2年やって思ったことをつらつらと

2021年2月末でWebエンジニアになってから丸2年経つので、ポエムです。ポエムは緊張しますね。

SIerで上流SE3年

マネジメント・SE業務について

  • Javaで動くアプリケーションを扱っていたのにもかかわらずろくにJavaでアプリケーションを1から作る経験などしていなかったのはやはり問題だった。土日を犠牲にしてでもしっかりPCにしがみついてコード書いとけばよかったなって今はめちゃくちゃ思ってます。多分今時を戻して当時担当してたアプリのコード読んだら楽しいと思う。読みたいなぁ

  • 今振り返ると、やはりオフショアと密にコミュニケーションが取れる中間マネージャを挟んだ上でのマネジメントだったからこそコードを書けない自分でもワークしていたなぁと思います。

  • オンサイトの外国籍メンバー(約2人)オフショアの外国籍メンバー(約20人)のマネジメントをする上で、ある程度自分はオンサイトメンバーからの要約を挟んだ情報共有をもとに意思決定や要件定義ができていたので、現地での実装で起きている非常に細かい問題に首をつっこむ必要があまりなかったです。なので意思決定がしやすかったなぁと。本当に当時は助かりました。。

  • 全体最適をする上でマネージャが行う格闘技みたいなものは一通りできて、かつ要件整理・顧客説明なども頻繁に行なったのでエンジニアとして必要なソフトスキルみたいなものはかなり磨かれた気がします。濃い時間だった。。あとオンサイトのインドメンバーもプロパーのメンバーもぶっちゃけ優秀な人ばかりだった。

  • 金融アプリケーションの保守の緊張感はもう今後の人生で2度と味わえないだろうなとは思いました。正直私はこの仕事は飛行機のパイロットと同じことをしてると思ってます。だからこそ身についた「危険なものを事前に嗅ぎ分ける」みたいなスキルはやっぱりあると思ってます。これは結構言語化むずいんですが、でも間違いなく存在しますね。

英語について

  • 改めて、業務時間中に外資系でもないのに日常的に英語を使って開発者達と会話できていたのはラッキーだったなと。いまだに英語力はなんだかんだ衰えていない気もします(たまに夢で英語で話す)。TOEICも多分今受けても流石に900は超えると思う。

  • 英語力の衰えが勿体無いので(筋肉と同じ衰え方をすると思ってる)、2022年中にちゃんと少しずつ勉強を再開します。

  • キャリアにおいても「社内公用語が英語の環境でエンジニアとして働く」と言うのは継続的に意識したくて、流石に大分英語環境とご無沙汰になってしまったので、おそらく次の転職の条件に入れようと思ってます。正直○天くらいしか知らないんだけども・・😿

余暇の使い方ついて

  • 一切勉強しなかった&大体遊んでました。あと年に2回は海外旅行行ってました。夕飯は大体大戸屋だった。貯金すればよかった。
  • 休みの障害対応で、大阪の遊園地のライオンの前で監視を見たりしたことがあります。あと函館行きのフェリーに乗り込む直前でアラート鳴ってネットが切れるギリギリまでに障害対応をするとか。サイバー系映画か?

Webベンチャーでエンジニア2年

文化について

  • コードを書くという行為自体が楽しい&かつコードを書いている人のコミュニティの雰囲気がそもそも好きでいいです。

  • 若い人が多くてやっぱり仕事は進めやすいです。

  • SIer在籍時と相対的に見て人的リソースの最適化ができてない状況(?)に対するアラート・喝をいれる人が結構少ないのが文化の違いなのかな、と思ったりします。目的のふわっとした打ち合わせとかが結構入ることが多い印象です。が、これは良し悪しあると思っててどちらが絶対いいと言う訳ではない。労働者としてはある程度自由にふわっとやれた方が上手くいくことは絶対あります。自由な発想の議論とかやっぱりいい案出すためには大事ですし。

業務について

  • 正直、Webエンジニアという職業は職務内容が好きなのであれば最高だと思います。日々思いますが、勉強さえすれば間違いなく力がついてアウトプットの品質が間違いなく上がり市場価値が上がっていくので。

    かつ、現状自分はその勉強をある程度趣味として捉えて取り組めているのでそれが一番予想外でした。正直、もう少し辛い気持ちになる事が多いのかと思ってました😹 コードを書き始めたのも遅いですしね。

    つまり何が言いたいかと言うと、2年前Webエンジニアとして転職する時に想定していた熱量よりも高く技術の勉強をする習慣が身につき、それを楽しめているといった感じです。

    2年半前ごろ、当時自分はデベロッパー達をマネジメントしているにも関わらずまともにコードを書いてアプリケーションを作った経験がなく、その状態でPJを進めていくことに強いストレスとコンプレックスを感じていました。かつ、ちょうどその時期から土日にコードを書くようになりそれが楽しく思えたことからWebエンジニアに転向することを決めました。

    当時は「ある程度技術を身につけたらSIerに戻って上流工程メインの仕事に戻ってもいいかも」と思っていましたが、今は正直そういう思いは消えてしまいました。。Webエンジニア、ある程度まともな給料がもらえる仕事の割には面白すぎますね。。

  • SIerでは間違いなく携われないような面白いドメインの開発ができるチャンスがWeb業界にはあります。3年前の自分に「お前は音楽サービスの開発をしている」なんて伝えたらひっくりかえる気がします。

余暇の使い方について

  • 昔は遊んでましたが(遊ぶことが悪いなんて一切思ってない。土日なんだから。)、ここ1年くらい土日両方の朝〜昼頃は喫茶店で勉強をし、午後から妻と遊びに行ったりたまに友人と遊ぶ、という生活を繰り返している気がします。友達と会う頻度はなんだかんだ減った気がします。

  • 本当は、連休使ってタイにフラッと旅行にいって自由気ままに旅しながら現地のコワーキングスペースで開発をしてみる&テック系イベントに参加するとか、転職の合間にある一定の無職期間を作ってフリーの仕事をしながら妻とヨーロッパを回る、みたいなことがしたいです。めちゃくちゃしたいんや。コロナウイルスはいつまで生き残るんでしょうね😿

上流SIerかWeb系どちらがいいか、みたいな話

  • これまで3人ほどSIer時代の友人経由で、NTT系の会社やホニャララチュアでSEをやっている方などから「Webエンジニアにキャリアチェンジしたい」と言った内容の相談を受けたりしてきたんですが、まぁ正直給与の話とか本人のキャリアプランも様々だろうし、本当に様々な比較軸があって意思決定しづらいですよね。

    私は結局のところ「Webエンジニアの方が楽しい!」になっているところはありますが、本当に人それぞれなので、同じキャリア(SIerのコードを書かない上流SE => コードを書くWebエンジニア)を辿った人とじっくり会話するなど参考情報をできる限り集めた上で考えるべきかなと思います(キャリア上のインパクトがなかなかでかいし)

    ちなみにここに書いている通り私も SIerのコードを書かない上流SE => コードを書くWebエンジニア な人間なので、細かいことや悩みについて相談したいとかがあれば TwitterMeety でぜひ話しましょう😸

【TypeScript】サブタイプとスーパータイプ、そして変性について

サブタイプとスーパータイプ

TypeScript上での型の間の関係についての概念である サブタイプ スーパータイプ についてです。

サブタイプ(subtype)

A、Bという2つの型があった場合に、BがAの派生型である場合、Aが要求されるところではどこでも、Bを利用することができる。

  • 配列はオブジェクトのサブタイプ
  • タプルは配列のサブタイプ
  • 数値 1 は numberのサブタイプ

スーパータイプ(supertype)

サブタイプの逆です。

A、Bという2つの型があり、BがAの上位型である場合、Bが要求されるところではどこでも、Aを安全に使うことができる。

  • オブジェクトは配列のスーパータイプ
  • 配列はタプルのスーパータイプ
  • number は 数値 1のスーパータイプ

変性とは?

ほとんどの基本型やシンプルな型(number, string, boolean...)については、なんらかの型Aが別の型Bのサブタイプであるかどうかを判別することは簡単ですね。例えば、 number | string という型があった場合にこれが string のスーパータイプであることは明らかです。

次に以下のようなオブジェクトの例を見てみます。以下のような例を考えます。

type LegacySong = {
  id?: number | string;
  name: string;
};

function deleteSongId(song: { id?: number; name: string }) {
  delete song.id;
}
const legacySong: LegacySong = {
  id: 123456,
  name: "old song",
};
//  Type 'string | number | undefined' is not assignable to type 'number | undefined'.
//  Type 'string' is not assignable to type 'number | undefined'.
deleteSongId(legacySong);

deleteSongId()が引数として期待しているオブジェクトの型と実際に渡されているとの間に問題が生じていることがわかります。

具体的に言うと、deleteSongId() は引数として期待するオブジェクトのプロパティ idnumber | undefinedであることを期待していますが、実際に呼び出し部分で渡されているlegacySongのプロパティ id は 'string | number | undefined'になっています。

TypeScriptはこのように、関数の引数やあらゆるシチュエーションにおいてオブジェクトが渡されることが期待される場合、期待される型のスーパータイプであるプロパティの型を持つ形状は渡すことができません。そしてこの性質を共変と呼びます。

そして、共変は、4種類の変性(variance)のうちの1つにすぎません。以下にその4種類の変性をまとめます。

  • 不変性(invariance)
    • Tそのものが必要
  • 共変性(covariance)
    • Tそのもの、もしくはサブタイプが必要
  • 反変性(contravariance)
    • Tそのもの、もしくはスーパータイプが必要
  • 双変性(bivariance)
    • Tそのもの、もしくはスーパータイプ、サブタイプが必要

関数の変性

関数Aが関数Bのサブタイプであるためには、以下を満たす必要があります。

  • 関数Aが関数Bと同じかそれより低いパラメーターの数を持つ。
  • 関数Aのthis型が指定されていない、または「Aのthis型がBのthis型のスーパータイプ」である。
  • 対応するそれぞれのパラメーターについて「Aのパラメーターの型 が Bのパラメーターのスーパータイプ」である。
  • 「Aの戻り値の型 が Bの戻り値のサブタイプ」である。

それぞれの構成要素(this型、パラメーターの型、戻り値の型)について、単にサブタイプでなのかな・・・と思いきやそれに準ずる要素は戻り値の型だけで、要素によって性質が反転する必要がありますね。なぜこのようなことになるのでしょうか?

以下の例で考えてみましょう。まず検証を行うために、3つのクラスを用意します。

class Pokemon {}

//DenkiPokemonはPokemonのサブタイプ
class DenkiPokemon extends Pokemon {
  electricShock() {
    console.log("電気ショック!");
  }
}

//PikachuはDenkiPokemonのサブタイプ
class Pikachu extends DenkiPokemon {
  lightning() {
    console.log("雷!");
  }
}

次に、以下ような1つの関数を引数として期待するコールバック clone()を定義します。

cloneに対してdenkiToDenkiを渡した時にエラーが起こらないことは明らかですね。じゃあ、denkiToPokemonはどうなんだと言うと、これはエラーを起こしてしまいます!😿

const clone = (f: (b: DenkiPokemon) => DenkiPokemon): void => {
  const parent = new DenkiPokemon();
  const babyDenkiPokemon = f(parent);
  babyDenkiPokemon.electricShock();
};

const denkiToDenki = (b: DenkiPokemon): DenkiPokemon => {
  return b;
};

const denkiToPokemon = (d: DenkiPokemon): Pokemon => {
  return d;
};

clone(denkiToDenki); // OK
clone(denkiToPokemon); // Argument of type '(d: DenkiPokemon) => Pokemon' is not assignable to parameter of type '(b: DenkiPokemon) => DenkiPokemon'.

clone()の処理を確認してみましょう。仮に渡す関数fがPokemonを返すとすると、処理の中で呼んでいる電気ショックをクラスPokemonは覚えていないので、実行することができないことがわかると思います。こういった理由から、TypeScriptは、渡された関数が「少なくとも」DenkiPokemonないしそのサブタイプを返すことを、コンパイル時に確認する必要があるのです。

一方で、パラメータについてはどうでしょうか。同じように確認してみます。

const clone = (f: (b: DenkiPokemon) => DenkiPokemon): void => {
  const parent = new DenkiPokemon();
  const babyDenkiPokemon = f(parent);
  babyDenkiPokemon.electricShock();
};

const pokemonToDenki = (b: Pokemon): DenkiPokemon => {
  return new DenkiPokemon();
};

const pikachuToDenki = (d: Pikachu): DenkiPokemon => {
  d.lightning();
  return new DenkiPokemon();
};

clone(pokemonToDenki); // OK
clone(pikachuToDenki); // Argument of type '(d: Pikachu) => DenkiPokemon' is not assignable to parameter of type '(b: DenkiPokemon) => DenkiPokemon'.

こちらも先程のケースと同様の考え方になります。 仮に関数のパラメータにDenkiPokemonのサブタイプであるPikachuを渡してしまうと、実際に関数がclone()の中で呼ばれる際にクラスDenkiPokemonlightning()を覚えていないのでエラーを吐くことになります。なので、TypeScriptは、渡された関数のパラメータが「少なくとも」DenkiPokemonないしそのスーパータイプを返すことを、コンパイル時に確認する必要があるのです。

これは、関数がそのパラメーターおよびthisの型に関して反変(contravariant)であることを意味します。つまり、ある関数が別の関数のサブタイプであるためには、それぞれのパラメーターおよびthisの型が、もう一方の関数で対応するものに対してスーパータイプである必要があります。

まぁ実際の現場の開発ではこのルールを覚えて実装する、なんて必要はないと思いますが、この制約が生まれる背景はしっかり抑えとくといいのかなと思いました。

※ちなみにTypeScript の関数は、それらのパラメーターおよび this の型に関して、デフォルトで共変らしいです。より安全な反変な振る舞い を選択するには、tsconfig.jsonの中で、{"strictFunctionTypes": true}フラグを有効にしてあげればOKです。

参考文献

www.oreilly.co.jp

【TypeScript】【React】【Recoil】翻訳しながらそれなりに型をつけて細々と進めていくRecoil Tutorial その4 Asynchronous Data Queries編(2)

f:id:lamechang_dev:20220213113950p:plain

本記事は、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリである Recoil の公式チュートリアルを翻訳しつつ、TypeScriptで型付けをしながら進めていく連載の第四弾です。細々とやっていきます🙄 今回は Asynchronous Data Queries の内容について書いていきます。前回が(1)で、今回は(2)に当たります。

recoiljs.org

Concurrent Requests

If you notice in the above example, the friendsInfoQuery uses a query to get the info for each friend. But, by doing this in a loop they are essentially serialized. If the lookup is fast, maybe that's ok. If it's expensive, you can use a concurrency helper such as waitForAll to run them in parallel. This helper accepts both arrays and named objects of dependencies.


上記の例で気付くかもしれませんが、friendsInfoQuery()はQueryを使用して各友達の情報を取得します。ただし、これをループの中で実行することにより、基本的に逐次的に処理されます。探索が速い場合は、おそらくそれで問題ありません。処理に時間がかかる場合は、waitForAllなどの同時実行ヘルパーを使用してそれらを並行して実行できます。このヘルパーは、依存する配列と名前付きオブジェクトの両方を受け入れます。


なるほど、下記のコードの通りに selectorが依存している selectorFamily である useInfoQuery の配列を waitForAll() の引数として渡すことで、内部で実行される非同期処理を逐次処理ではなく並行処理として扱うことができる、ということでしょうね。

json-serverを利用していると非同期の挙動がterminal上での出力することができますが、それを利用すると並列処理が走り効率的に非同期を処理していることがわかりやすいです。

f:id:lamechang_dev:20220223162257g:plain

export const friendsInfoQuery = selector<Array<User>>({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friends = get(waitForAll(
      friendList.map(friendID => userInfoQuery(friendID))
    ));
    return friends;  },
});


You can use waitForNone to handle incremental updates to the UI with partial data


waitForNoneを使用して、部分的なデータを使用したUIの増分更新を処理できます

waitForNone は渡された依存関係の状態の Loadable オブジェクトを返します。 例えば今回の例のように依存関係にある selectorFamily userInfoQuery の配列について、並列的にそれぞれの非同期を実行しPromiseが解決したものから部分的にUIを更新する、といったような挙動を以下のようなコードで実現することができるみたいです。

ちなみに、Loadable の詳細はこちらLoadable オブジェクトはatom もしくは selectorの現在の状態を表現しており使用可能な値があるか、エラー状態にあるか、非同期解決が保留されている可能性があります。これらはそれぞれhasValuehasError、または loadingで表現されます。

以下の例の friendsInfoQueryreturn部分を見ると、取得した friendsのうち statehasValueであるもののみにfilterをしていることがわかります。

const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friendLoadables = get(waitForNone(
      friendList.map(friendID => userInfoQuery(friendID))
    ));
    return friendLoadables
      .filter(({state}) => state === 'hasValue')
      .map(({contents}) => contents);
  },
});

Pre-Fetching

For performance reasons you may wish to kick off fetching before rendering. That way the query can be going while we start rendering. The React docs give some examples. This pattern works with Recoil as well.


パフォーマンス上の理由から、レンダリングの前にフェッチを開始したいと考えるかもしれません。そうすれば、レンダリングを開始するときにクエリを実行できます。 Reactのドキュメントにはいくつかの例があります。このパターンはリコイルでも機能します。


Let's change the above example to initiate a fetch for the next user info as soon as the user clicks the button to change users:


上記の例を変更して、ユーザーがボタンをクリックしてユーザーを変更するとすぐに、次のユーザー情報のフェッチを開始しましょう。

これ、突然 useRecoilCallback が登場してきてるので、そちらの挙動の確認をした方がいいでしょう。

さてuseRecoilCallbackですが、こちらは subscribeさせずに atomもしくは selector を読み込んで何かをする、という時に利用されると考えます。subscribeによる状態管理は、値の変更を常に応じて状態を常に更新する必要が出てきてしまいますが、下記の例のように onClickが走った時のみに値を取得し、再レンダリングを行いたい、といったことを実現することができます。

const CurrentUserInfo: React.VFC = () => {
  const currentUser = useRecoilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);

  const changeUser = useRecoilCallback<Array<number>, void>(({snapshot, set}) => userID => {
    snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info
    set(currentUserIDState, userID); // change current user to start new render
  });

  return (
    <div>
      <h1>{currentUser.name}</h1>
      <ul>
        {friends.map(friend =>
          <li key={friend.id} onClick={() => changeUser(friend.id)}>
            {friend.name}
          </li>
        )}
      </ul>
    </div>
  )};

export default CurrentUserInfo;


Note that this pre-fetching works by triggering the selectorFamily() to initiate an async query and populate the selector's cache. If you are using an atomFamily() instead, by either setting the atoms or relying on atom effects to initialize, then you should use useRecoilTransaction_UNSTABLE() instead of useRecoilCallback(), as trying to set the state of the provided Snapshot will have no effect on the live state in the host .


このプリフェッチは、selectorfamily()による非同期クエリの実行と、selectorのキャッシュを入することによって機能します。代わりにatomFamily()を使用している場合は、Atomを設定したり、atoms effectsによる初期化を利用することで、userecoilCallback()の代わりにuseRecoilTransaction_UNSTABLE()を使用してください。提供されたSnapshotのstateを設定しようとしても、RecoilRootのstateには影響しません。


Asynchronous Data Queries の部分は新しい情報がモリモリですね。Query Default Atom Values以降については更に記事を分けて、(3)としたいと思います。