なぽろぐ

気ままに感じたことを記事にまとめます。

よく使いそうなtemplate-literal-typesを使った型定義

いつもTSのPlaygroundで作ってるなぁと思うやつをメモする

下に書いてあるやつすべてplaygroundで実行しているやつ

www.typescriptlang.org

splitの型定義

// 文字列を受け取って受け取ってその文字列をDelimiterでsplitする型
type Split<T, U extends string> = T extends string
                                    ? T extends ""
                                      ? []
                                      : T extends `${infer Head}${U}${infer Tail}`
                                        ? [Head, ...Split<Tail, U>]
                                        : [T]
                                    : []
type UseSplit1 = Split<"aaaaaa", ""> // 1文字づつ区切る
type UseSplit2 = Split<"abababab", "b"> // 特定文字で区切る
type UseSplit3 = Split<"a,b,c", ","> // csv的なやつ

nestしたobjectのkeyを列挙する型定義

// nestしたオブジェクトのkeyをすべて列挙する型
// json to csv的なやつに使えそうかも
type NestObjectKeys<T, D extends string = "."> = {
  [K in keyof T]: K extends string ? T[K] extends Record<string, unknown> ? `${K}${D}${NestObjectKeys<T[K], D>}` : K : never
}[keyof T]

const obj1 = {
  a: 1,
  b: null,
  c: "c",
  d: {
    a: 1,
    b: undefined,
    c: "dc",
    d: {
      a: 2,
      b: null,
      c: "ddc"
    }
  }
} as const
type Keys1 = NestObjectKeys<typeof obj1> // objectのkeyを.でつなげる
type Keys2 = NestObjectKeys<typeof obj1, "-"> // 特定の文字でつなげる

よくあるrouterライブラリのpath文字列を型に落とし込む型定義

// あるあるのrouterライブラリのpath文字列を型に落とし込む型
type AnalyzePath<T> = T extends string
                      ? T extends `/${infer Head}/${infer Tail}`
                        ? Head extends `:${infer U}`
                          ? { [K in U]: string } & AnalyzePath<`/${Tail}`>
                          : AnalyzePath<`/${Tail}`>
                        : T extends `/:${infer U}`
                          ? { [K in U]: string }
                          : {}
                      : {}
type AnalizedPath1 = AnalyzePath<"/users/:user_id/">
type AnalizedPath2 = AnalyzePath<"/users/:user_id/tweets/:tweet_id">

Nextjsのpath文字列を型に落とし込む型

これ型に落とし込めてもnextjsはfsでroutingされているからあまり意味ないんだよなぁと言うのが最近の悩み。ずっと考えてるけどいい方法思いつかないなぁ.....next.config.jswithXXXコンパイラをいじったりしていい感じにするとかかなぁ....わかりません

// Nextjsのpath文字列を型に落とし込む型
type AnalizeNextJSPath<T> = T extends string
                            ? T extends `${infer Head}[${infer U}]${infer Tail}`
                              ? U extends `...${infer U1}`
                                ? { [K in U1]: string[] } & AnalizeNextJSPath<Tail>
                                : { [K in U]: string } & AnalizeNextJSPath<Tail>
                              : {}
                            : {}
type AnalizedNextJSPath1 = AnalizeNextJSPath<"/users/[id]/">
type AnalizedNextJSPath2 = AnalizeNextJSPath<"/users/[id]/directory/[...directories]/">

snake caseをcamel caseにする型

// snake case to camel case
type SnakeToCamel<T> = T extends string
                        ? T extends `${infer Head}_${infer Tail}`
                          ? `${Lowercase<Head>}${Capitalize<Tail>}`
                          : never
                        : never
type UseSnakeToCamel = SnakeToCamel<"user_id" | "tweet_id">

camel caseをsnake caseにする型

// camel case to  snake case
type CamelToSnake<T> = T extends string
                        ? T extends `${infer Head}${infer Tail}`
                          ? Head extends Uppercase<Head>
                            ? `_${Lowercase<Head>}${CamelToSnake<Tail>}`
                            : `${Head}${CamelToSnake<Tail>}`
                          : T
                        : never
type UsecCamelToSnake = CamelToSnake<"userId" | "tweetId">

まとめ

便利ですね。template-literal-types。一生遊べる

ここ1年で買ってよかったものシリーズ

買ってよかったものシリーズ2020

1. ルンバi7

store.irobot-jp.com

最近買ったけどこれが最高すぎる。起動1、2回目までは30分くらい掃除してるんですが3回目くらいからは15分くらいで掃除してくれます。音は結構大きい。ルンバに掃除させながらAPEXをやると何も聞こえなくて普通に背後に寄られて死ぬのが容易になる。まぁ普通に掃除機なのでそれはそうだね。

地形を覚えてくれて仕切りを自分で設定して仕切りごとに掃除できる。これが便利〜〜〜いいね

f:id:Naporitan:20201201180602p:plain

2. hue

www.philips-hue.com

正社員になってからコロナの影響でフルリモートだったので気持ちの切り替えが大変だったんですが、これで割と僕は解決しました。始業時間と就業時間になったらライトの色を変えるように設定していたのでスパッとモード切替できてよかった〜。これもおすすめ。Amazon Echoとの相性もとてもいい。最高

3. ウォーターサーバー

premium-water.net

ウォーターサーバー自体はどこでもいいじゃないかと思ってるけど僕はこれ使ってます。温水と冷水が常に出てくるのはめちゃくちゃいいですね〜。東京は想像以上に水道水が飲めたものじゃないのでこれがあると家がペットボトルまみれにならないのが最高です。まじでいい。ごみ捨てが楽になるので。

4. webカメラ

www.logicool.co.jp

別にストリーマーってわけじゃないけど無駄にいいカメラを買った。カメラは最高すぎる。自分の顔を移したいわけじゃないが高画質のwebカメラはいろいろな遊び道具になるからね。いいもの!

5. マイク・オーディオインターフェース(これは前から持ってた)

www.marantzpro.jp

フルリモートだったのでMTGすべてがオンラインだったので聞こえづらいとか相手の耳障りにならないようにコンデンサーマイクを買いました。オーディオインターフェースがあるとハードウェアミュートがあるので色々便利ですよ。ミュートマークが出ずにミュートできるので〜

さいごに

今年も色々買った。AirPods ProとかMacBookProもかったけどフルリモートという関係上あまり効力を発揮せずランクインならずって感じです。ほと〜〜〜〜んど家にいたからね。来年は椅子とディスプレイを買いたいです。あとキーボードかな〜キーボード。ゴミ箱からひろってきたHHKBを漂白して使ってるのでそろそろ新品をね......

デカ枕を買ったら腰痛くなくなったんだがwwwwwwwwww

腰が痛ぇ!

起きたら腰痛いし、常に椅子に座って作業してるのでどう考えても運動不足で常に腰に負担をかけ続けている生活を送ってたし、健康診断に行ったら基本的には健康ですが背骨が曲がってますねぇ......って言われるくらい腰が終わってました。

あまりに痛いので整体にここ1ヶ月くらい通ってたのですが、そこでおすすめされた枕を購入してみるとまじで劇的な変化があったのでおすすめしておきます。全腰痛民族に捧ぐ.....

shopping.karada39.com

僕は枕が終わってたために腰を痛めていたようでした。前買った枕3年くらい使ってたものだったのでそれはそうかも知れない.....

これ高さ調整ができる(中のクッションが層になっている)のである程度自分用に調整できます。

2週間くらいつかってみた結果ですがまじで調子がいい。ありえないくらい調子がいい。無限に作業ができるし座ってられる。あと睡眠欲がめっちゃ増えた(睡眠による回復がとてつもないため)

さいごに

枕は1年、2年おきくらいに変えたほうがええ

実際に使っている様子を写真で取ろうと思ったけど掃除してないのでやめた。部屋が汚ねぇ

チケに嫌われている

去年のにじさんじのVirtual To Live以来応募してるチケットにすべて落選している........

徳の積み方、チケットが当たるおまじない等様々な意見募集してます

意見があった徳の積み方・チケットが当たるおまじない

  1. 毎日脳内でゴミ拾いしなきゃ

reselectのメモ化について

reselectのメモ化がどうなってるのか知りたい

github.com

ので調べました。コード自体は100行くらいで1ファイルだけだったのでとても読みやすかったです。

reselectはreduxのためのselectorライブラリです。どうやらメモ化してくれるらしいが.....?

reselectは受け取るオブジェクトを整形する関数と結果を計算する関数を受け取って、selectorを作る関数です。

以下の例だと、state => state.itemsstate => state.selectIdがオブジェクトを整形する関数、(items, id) => items.find(item => item.id === id)が結果を計算する関数です。

stateが変更したときどこが再計算されて、どこがメモ化されるのかを知ることでreselectの力を最大限使おうというのが本記事の意図です。

import { createSelector } from "reselect";
const state = {
  items: [
    { id: 1, name: "hoge" },
    { id: 2, name: "huga" },
    { id: 3, name: "nyan" }
  ],
  selectId: 1,
};

type State = typeof state;
type Item = State["items"] extends Array<infer U> ? U : never;
const findUser = createSelector<State, State["items"], State["selectId"], Item | undefined>(
  state => state.items,
  state => state.selectId,
  (items, id) => items.find(item => item.id === id)
)

どこがメモ化されるのか先に言うと、最後の関数(items, id) => items.find(item => item.id === id)です。この子だけstateが変更されたときに計算がスキップされることがあります。それ以外の関数state => state.items, state => state.selectIdに関しては毎回計算されます。では実際にどのようなコードで実現されているのかを見ていきます。

reselectのコードを見ていく

createSelector

まずはcreateSelector。いつもimportして使う関数から見ていきます。これは単にcreateSelectorCreatordefaultMemoizeを食わせているだけのようです。何もわかりませんね。

reselect/index.js at master · reduxjs/reselect · GitHub

export const createSelector = /* #__PURE__ */ createSelectorCreator(defaultMemoize)

defaultMemoize

どうやら関数と比較関数を受け取る関数みたいです。比較関数defaultEqualityCheckは単にa === bをするだけのコードでした。気になる方は見てみてもいいかもしれませんが、ほんとにこれだけです。

areArgumentsShallowlyEqualに比較関数(equalityCheck)、前回入力された引数(lastArgs)、今回入力された引数(arguments)を食わせます。前回と今回入力された引数を比較してshallowEqualがfalseならfuncを再計算してlastResultに再代入するという感じでしょう。

argumentsに関してはjavascript組み込みなのでMDNを見るといいと思います。

reselect/index.js at master · reduxjs/reselect · GitHub

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // we reference arguments instead of spreading them for performance reasons
  return function () {
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null, arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}

areArgumentsShallowlyEqual

equalityCheckは比較関数、prev, nextは配列でやってくるようですね。配列の要素を総ナメして同じ値ならtrueそうでないならfalseを返すだけの関数です。

reselect/index.js at master · reduxjs/reselect · GitHub

function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
  if (prev === null || next === null || prev.length !== next.length) {
    return false
  }

  // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
  const length = prev.length
  for (let i = 0; i < length; i++) {
    if (!equalityCheck(prev[i], next[i])) {
      return false
    }
  }

  return true
}

ここまででわかったことをまとめます。

  • defaultMemoizeは複数の引数を受け取る関数と比較関数(optional)を受け取る
  • defaultMemoizeは複数の引数を受け取る関数を返す

こんな感じでしょうか。次に行きましょう。

createSelectorCreator

ここでのmemoizeはdefaultではdefaultMemoizeです。...memoizeOpptionsはユーザがmemoize関数を定義したときoptionを渡したときに使うものです。今回はdefaultMemoizeが使われる前提なのでいらない子として扱います。

この関数では受け取ったデータ整形用の関数と結果生成用の関数からメモ化された関数を返します。

変数名で言うとresultFuncが結果生成用の関数。dependenciesがデータ整形用の関数です。

memoizedResultFuncは結果出力関数をdefualtMemoizeでラップしただけのものなのでselectorだけを見ていきましょう。

reselect/index.js at master · reduxjs/reselect · GitHub

export function createSelectorCreator(memoize, ...memoizeOptions) {
  return (...funcs) => {
    let recomputations = 0
    const resultFunc = funcs.pop()
    const dependencies = getDependencies(funcs)

    const memoizedResultFunc = memoize(
      function () {
        recomputations++
        // apply arguments instead of spreading for performance.
        return resultFunc.apply(null, arguments)
      },
      ...memoizeOptions
    )

    // If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
    const selector = memoize(function () {
      const params = []
      const length = dependencies.length

      for (let i = 0; i < length; i++) {
        // apply arguments instead of spreading and mutate a local list of params for performance.
        params.push(dependencies[i].apply(null, arguments))
      }

      // apply arguments instead of spreading for performance.
      return memoizedResultFunc.apply(null, params)
    })

    selector.resultFunc = resultFunc
    selector.dependencies = dependencies
    selector.recomputations = () => recomputations
    selector.resetRecomputations = () => recomputations = 0
    return selector
  }
}

selector

このselectorcreateSelector()の返り値そのものです。なのでこの子が実データ(一番はじめの例でいうとstate)を受け取ります。

与えられた実データをargumetns経由で取り出します。それをデータ整形用の関数(dependencies)に食わせます。このときdependenciesの関数たちに引数を適用する計算はselector実行時に毎回呼ばれるのでデータ整形用の関数はコストの小さい関数を置いておくのがいいでしょう。

こうして得られた配列paramsmemoizedResultFuncに与えます。paramsの結果が前回と同じであれば計算をスキップし、前回の結果を返します。reselectにおいてここだけが計算が省略される部分というわけでした。

reselect/index.js at master · reduxjs/reselect · GitHub

const selector = memoize(function () {
  const params = []
  const length = dependencies.length

  for (let i = 0; i < length; i++) {
    // apply arguments instead of spreading and mutate a local list of params for performance.
    params.push(dependencies[i].apply(null, arguments))
  }

  // apply arguments instead of spreading for performance.
      return memoizedResultFunc.apply(null, params)
})

まとめ

  • 計算がスキップされるのはcreateSelectorで渡す引数のうち最後の関数だけ!
  • 最後の関数以外は毎回計算される

この2つだけわかれば十分すぎるくらいです。reselect完全に理解した!ですね!

reselectメモ化してくれることは知っていたのですが、どこメモ化されるのか全くわからずuseSelectorに食わせる関数を作ってくれる君程度にしか思ってなかったのでちゃんと理解できてよかったです。

あとarguments。存在を知らなかったので突然定義してないやつが出てきた!?!?!?と凄い驚きました。

おわりです。ありがとうございました。