TypeScriptのmapped typesでネストされた型もすべてオプショナルにする

partial<T>

TSのmapped typesを利用して提供されている便利な型partial<T>がある

type Partial<T> = {
    [P in keyof T]?: T[P];
};

これが提供されているので

例えば以下のようなfoo型のプロパティをすべてオプショナル(?:の形式)にしたかったらこうすればよい。

interface foo {
  foo: string
  bar: string
  baz: number
}

type optionalFoo = partial<foo>

とすると

interface optionalFoo {
  foo?: string | undefined
  bar?: string | undefined
  baz?: number | undefined
}

と同じことをしていることになる。

便利。

ネストがある場合問題あり

partial<T>便利なのだけど、ネストした型がある場合、ネストされたやつまではoptionalな形式に変換してくれない。ネストされたものもすべてオプショナルに変換したいときこれでは困る。

interface foo {
  foo: string
  bar: string
  baz: {
    hoge: string
    piyo: string
    fuga: number
  }
}

こういう状態のやつ

これをpartial<T>すると

interface optionalFoo {
  foo?: string | undefined
  bar?: string | undefined
  baz?: {
    hoge: string
    piyo: string
    fuga: number
  }
}

となってしまう。

解決方法

俺はネストされた型もすべてオプショナルにしたいんだよねという場合はpartial<T>がrecursiveな挙動をするようなmapped types型を自分で書いてやればOKらしい

stackoverflow.com

type NestedPartial<T> = {
    [K in keyof T]?: T[K] extends Array<infer R> ? Array<NestedPartial<R>> : NestedPartial<T[K]>
}

TS2.8以降はこうすればArrayがあっても大丈夫とのことらしい。

というわけで

type NestedPartial<T> = {
    [K in keyof T]?: T[K] extends Array<infer R> ? Array<NestedPartial<R>> : NestedPartial<T[K]>
};

interface foo {
  foo: string
  bar: string
  baz: {
    hoge: string
    piyo: string
    fuga: number
  }
}

type optionalFoo = NestedPartial<foo>

こうすればネストされた型も全部オプショナルに変換された。

ありがとうstackoverflow 🙏🙏🙏

axiosのparamsに階層の深いネストしたオブジェクトを渡したい

表題どおり

paramsにネストの深いオブジェクトを設定してGETメソッドでサーバーとやりとりしたかった。

普通にやるとJSONにされるのでサーバーで受け取ったとき string になってしまってオブジェクトとして取り扱えない。

qsを使ってparamsSerializerに設定する

github.com

qsはこれ

github.com

  // `params` are the URL parameters to be sent with the request
  // Must be a plain object or a URLSearchParams object
  params: {
    ID: 12345
  },

  // `paramsSerializer` is an optional function in charge of serializing `params`
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  }

axiosのリポジトリから引用

てな具合なので、実際の感じはこう

const config = {
        method: 'get',
        params: { deepObject, tooDeepObject, deeeeeeeeeepObject },
        paramsSerializer(params) {
          return qs.stringify(params)
        },
        responseType: 'json'
      }

      const fetch = await axios('/hogehoge', config)
      return fetch

POSTメソッドの場合は『application/x-www-form-urlencoded』

axiosのREADMEの"Using application/x-www-form-urlencoded format"の項を参照して同じくqsを使えばOK

Node.js Expressのエラーハンドリング&next()の知見

主にExpress v4でのはなし。

Expressでのエラー処理全然わからなくて困っていたけど、ちょっとわかってきた気がするのでメモついでに書く。

くらいの人向けを想定して書いている。

便利なモジュール

boomはエラーオブジェクトユーティリティライブラリ。

github.com

hapijsのモジュールだけどExpressでも使えるので使っていく。

boomはようは『適切なエラーオブジェクト生成君』でエラーを出したいときに何のエラーかを区別する必要があったりするけど、boomを通すとラクになる。

例えば

Boom.forbidden('try again some time'); という感じにboomのforbiddenメソッドを呼ぶと

{
    "statusCode": 403,
    "error": "Forbidden",
    "message": "try again some time"
}

こういうエラーオブジェクトを作ってくれる。こっちでステータスコードをどうこう考えなくてよくなって便利。

エラー処理用ミドルウェアを作る

というわけでboomを用いたエラー処理ミドルウェア君を作っておく。

./middleware/error.js のようなファイルを作って

const errorHandler = (err, req, res, next) => {
  console.log('エラーハンドリング')
  if (res.headersSent) return next(err)
  if (!err.statusCode) err = boom.boomify(err)

  if (err.isServer) {
    // boom通した500番台のエラーはisServerでtrueが返るので
    // 500番台のみsentryみたいなエラー監視saasに送信したりできる
  }
  return err.isBoom
    ? res.status(err.output.statusCode).json(err.output.payload)
    : res.status(err.statusCode).json(err)
}

export default errorHandler

こういう感じのエラーハンドラーを作る。

boomを通したエラーオブジェクトはisServerisBoomなどのキーを持つので、これらによって「boomを介したエラーか」「500番台エラーか」とかの切り分けができる。

三項演算子でfalse側を用意しているのはboomを介していないエラーを投げたい時もあるはずなので、そういうときの処理のために書いている。 あとここではブログ的にエラーハンドラーの処理がなされたことがわかるようにconsole.logしているけど実際はいらない。

index.js 的なexpressの設定を書いているこういうファイルで、作ったエラーハンドラーを食わせるようにしておく

import express from 'express'
import errorHandler from '../middlewares/error'

const app = express()

// エラー処理ミドルウェア
app.use(errorHandler)

これで最低限のエラー処理機構ができた。エラーが想定されるところでエラーを投げると、エラー処理ミドルウェアがキャッチして処理してくれるようになった。

それではエラーを投げてみましょう。

そもそもどう書けばいいのか

エラーハンドラーのミドルウェアを書いたはいいけど、じゃあこのミドルウェアへ行くようにどう書けばいいのか。

エラーハンドリングのガイドは特に読みにくく、つらい。

『Expressでエラーを投げてエラー処理に行くには??』

それは

next(error)

です。

…と日本語の公式ガイドには"明確に"書かれていない…。

nextメソッドの第一引数になにかを入れると、エラー処理のミドルウェアにいきます…。

なお

if (err) {
    next(err); // Pass errors to Express.
}

なお英語版の公式ガイドにはこう書いてあるのでひと目で察しがつく

next(), next(error), next('route')について

公式ガイドに

next() 関数に (ストリング 'route' を除く) 何らかを渡すと、Express は、現在の要求でエラーが発生したと想定して、エラーが発生していない残りのすべての処理のルーティングとミドルウェア関数をスキップします。

next() および next(err) の呼び出しは、現在のハンドラーが完了したことと、その状態を示します。next(err) は、上記のようにエラーを処理するようにセットアップされたハンドラーを除き、チェーン内の残りのハンドラーをすべてスキップします。

こう書いてあるけど「?」ってなる

ようは

ということなのだけどわかりが悪い。

引数なしのnext()はわかるけど、引数ありはどういうことなのか。

next(error)

next(error)は

だった。

例としてこういうルーティングとミドルウェアを想定してみる。例なので isSuccess は強制的にfalseにしている。

returnなしパターン
router.get('/hello', (req, res, next) => {
    let isSuccess = false
    if (!isSuccess) next(Boom.badRequest('invalid query'))

    // returnしてないので表示される
    console.log('Logging: A')
  },
  // 後続のミドルウェア
  (req, res, next) => {
    // next(error)したので表示されない
    console.log('Logging: B')
  }
)

// 後続のルーティング処理
router.get('/hello', (req, res, next) => {
  // next(error)したので表示されない
  console.log('後続のルーティング処理だよ〜')
  next()
})

コンソールの表示はこう

Logging: A
エラーハンドリング

最初に紹介したBoomモジュールを使ってnextの第一引数にエラーを渡している。のでnext(error)したかたち。

エラー処理ミドルウェアの処理に移ったので「エラーハンドリング」が表示されている。

気をつけたほうがいいのはnext(error)した箇所でreturnしないとミドルウェア内の後続の処理は走ってしまう。だからLogging: Aが表示されている。

next(error)すると後続のミドルウェア・ルーティング処理はスキップされるので、てっきりthrow new Error()的な挙動をするのかと思うけど実際はそうではない。

なので、後続の処理をガードしたいならこう。

returnありパターン
router.get('/hello', (req, res, next) => {
  let isSuccess = false
  if (!isSuccess) return next(Boom.badRequest('invalid query')) // ここでreturn

  // returnしたので表示されない
  console.log('Logging: A')
  },
  // 後続のミドルウェア
  (req, res, next) => {
    // next(error)したので表示されない
    console.log('Logging: B')
  }
)

// 後続のルーティング処理
router.get('/hello', (req, res, next) => {
  // next(error)したので表示されない
  console.log('後続のルーティング処理だよ〜')
  next()
})

コンソールの表示はこう

エラーハンドリング

「Logging: A」が消えた。ミドルウェア内の後続処理をガードしたいならこのように適切にreturnしてあげないといけない。

next('route')

next('route')は

だった。なので以下のようになる

router.get('/hello', (req, res, next) => {
  let isSuccess = false
  if (!isSuccess) return next('route')

  // returnしたので表示されない
  console.log('Logging: A')
  },
  // 後続のミドルウェア
  (req, res, next) => {
    // next(error)したので表示されない
    console.log('Logging: B')
  }
)

// 後続のルーティング処理
router.get('/hello', (req, res, next) => {
  // next(error)はしたけどnext('route')なのでこれは表示される
  console.log('後続のルーティング処理だよ〜')
  next()
})

コンソールの表示はこう

後続のルーティング処理だよ〜

nextの第一引数になにかを渡しているのでnext(error)形式ではあるので後続のミドルウェアは実行されない。なので「Logging: B」は出ていない。

またnext(error)形式ではあるものの例外的にエラー処理には移らないので「エラーハンドリング」も表示されていない。後続のルーティング処理に移るので「後続のルーティング処理だよ〜」は実行されている。


next(), next(error), next('route')の挙動は難しい。


実際の処理の様子

そういうわけで実際には基本的には上記をふまえてこのように書いていくと思う。あくまでパターンの一例ですが例として書いておきます。

Expressアプリケーション側

import boom from 'boom'

router.use('/v1/hello', (req, res, next)=> {
  try {
    // なんかサーバー処理実行する
    next()
  } catch(error) {
    next(boom.clientTimeout('timed out'))

    //// boom通したくないならエラーオブジェクトをそのまま放り込む     
    // next(error)
  }
})

クライアント側

import axios from 'axios'

const awesomeFetch = async() => {
  try {
    const {data} = await axios(`/v1/hello`, {
          method: 'get',
          responseType: 'json'
        })
  } catch (error) {
    console.log(error.response)
  }
}

awesomeFetch()

axiosでフェッチ処理してエラーが発生(Express側でエラー処理がなされたら)、catch節にてこういう感じにエラーオブジェクトをcatchできます。

ここではboom.clientTimeout('timed out')タイムアウトエラーを発行しているので、

{
    "statusCode": 408,
    "error": "Request Time-out",
    "message": "timed out"
}

てな感じにエラー内容が取得できます。


とりあえず長いので今回はこの辺までで。

async/awaitを使ったPromiseベースのエラーハンドリングについてはもうちょいあるのでまたこんど書くかもしれない。(下記参考URLに載っていることがほとんどではあるけれど)

なんか間違ってるところがあるかもしれないのでなんかあったら逆に教えてほしいです。


【参考URL】

npm link なにが便利か

tl;dr

手元で開発中のモジュールをローカルの別のプロジェクトで使いたいときに便利


  • いまあなたは『my-great-module』というすごい便利なモジュールを作っているとする

  • すごい便利なモジュールなのだけど一部未完成なのでnpmにパブリッシュするのはまだためらっている

  • でもすごい便利なモジュールなのでβでもいいから手元で開発中の別のプロジェクトで早く使いたい

    • なんとかしたい

みたいなときに便利。

もしくは、

  • 公開中のnpmモジュールに新機能付けたいけどドッグフーディングしたい
    • まだnpmに公開はしたくなくて、ローカルで試したい
      • ブランチ切って試したい

みたいなときに便利。

npm link

docs.npmjs.com

使い方書いてある。

使い方ザッと書くと

  1. 利用元のモジュールへ cdして npm link
$ cd my-great-module
$ npm link
  1. 利用先で npm link <モジュール名>
$ cd my-develop-other-project
$ npm link my-great-module

注意点

  • npmのグローバル(npm -g)にシンボリックリンク貼って参照している
  • npm link <モジュール名>
  • スコープモジュールの場合は @scope/モジュール名形式
    • 要は npm i するときと同じ形式でlinkする

yarmもあるよ


ようは自前のnpmモジュールを手元でシュッと試したいときとかに便利ですね

TypeScriptのコンパイル時に『Cannot find name 'EventListenerOrEventListenerObject'.』と怒られる

Firebase Cloud FunctionsをTSで書いて使おうとしていて $ firebase init functions したあとにとりあえずHello Worldしようと思ってハマった。

以下のgetting-startedをのindex.tsをコピーして

github.com

$ firebase deploy --only functions

したところtscコンパイルでいきなりエラーが出てしまった。

node_modules/@types/history/DOMUtils.d.ts:2:78 - error TS2304: Cannot find name 'EventListenerOrEventListenerObject'.

こう怒られる

../node_modules/@types/react-dom/index.d.ts:19:72 - error TS2304: Cannot find name 'Text'.

加えてこうも怒られる

解決手段

tsconfig.json"lib""dom"を足す

こんな感じ

{
  "compilerOptions": {
    "lib": ["es6","dom"], ← "dom"を足す
    "module": "commonjs",
    "noImplicitReturns": true,
    "outDir": "lib",
    "sourceMap": true,
    "target": "es6"
  },
  "compileOnSave": true,
  "include": [
    "src"
  ]
}
参考情報

JavaScriptでオブジェクトの分割代入をするとき変数名を別の名前にする

オブジェクトの分割代入(Destructuring assignment)をするときに、変数名を自分の好きな名前に変更したいと思って調べたら、方法があった。

MDN

developer.mozilla.org

異なる名前を持つ変数への代入

var o = {p: 42, q: true};
var {p: foo, q: bar} = o;
 
console.log(foo); // 42 
console.log(bar); // true  

var { キー名 } = オブジェクトの形式で分割代入するところをvar { キー名: 任意名 } = オブジェクトとすることでOK。

このMDNの例だと

var { p } = o

var { p: foo } = o

としてpをfooに変更している。


なんだか慣れないとオブジェクトのkey: valueの構文もしくは型指定に見えてきてしまうので早く慣れたい。

ESモジュールのimport構文でimport { p as foo } from 'hoge' という構文があるから、asを使ったこのノリでできないのがなんでなんだろうなーという気がしている。深く調べてないのでECMAの仕様決定までの流れをいつか調べたい。

Reactでフォームの外側あるいは別コンポーネントのbuttonからsubmitする

FormikというReactのフォームライブラリがあり、それでFormを組んでいてハマったのでメモ。

github.com


欲求

Formの中にButtonを配置せず、『外側にサブミットボタンを配置したい』

ようはフォームの外側にonSubmitなボタンコンポーネントを作って、それを押したら目的のフォームがsubmitされるようにしたい。

解決策

github.com

react-final-formのFAQみてたら書いてあった。

① formのDOM取得してEventをディスパッチする

Via document.getElementById()

<button onClick={() => {
  document.getElementById('myForm').dispatchEvent(new Event('submit')) // ✅
}}>Submit</button>

<form id="myForm" onSubmit={handleSubmit}>
  ...fields go here...
</form>

なるほど!

クロージャでsubmt変数を定義してそこにsubmit関数を代入する

Via Closure

let submit
return (
  <div>
    <button onClick={submit}>Submit</button> // ❌ Not overwritten closure value
    <button onClick={event => submit(event)}>Submit</button> // ✅
    <Form
      onSubmit={onSubmit}
      render={({ handleSubmit }) => {
        submit = handleSubmit
        return <form>...fields go here...</form>
      }}
    />
  </div>
)

たしかに〜

③ そもそもHTML5のform属性を利用できる

これは別途react-final-formのFAQには書いてないけどやってみたらできた別案。

HTML5にはform属性があって、例えばMDNのbuttonのページにはこうある

ボタンに関連付けられた form 要素(form owner)です。 属性値は同一文書内の

要素の id 属性と同一の値にしなくてはなりません。 この属性を指定しない場合は、祖先に 要素が存在すれば、その要素に関連付けられます。 この属性によって 要素の子孫にするだけでなく、同一文書内にある任意の <form> 要素に <button> 要素を関連付けることが可能になりました。

ようするにformのid属性と関連付けが出来る

ので、こう書けばformの中にbuttonがなくともsubmitが可能

<button form='myform'>Submit</button>

<form id="myForm" onSubmit={handleSubmit}>
  ...fields go here...
</form>

ReactでFormむずかしい

アプリっぽいUIを作っていて、フォームの送信ボタンをヘッダーの右上(スマホの右上)に設置したくて悩んでいて、この解決方法になんとかたどり着いた。

Reactでフォーム組むのとにかく難しくて、どこに処理書けばいいとかcontrolledかuncontrolledかとかバリデーションとかいろいろある。様々な概念とライブラリの仕様が絡み合うから厳しい気持ちになる。