Open6

【TypeScript】空のオブジェクトにプロパティを後付けするときの型指定

ながなが

JavaScriptというか動的型付けの言語だと、オブジェクトを作るときに

function getObj() {
  const obj = {}
  obj.foo = 'a'
  obj.bar = 'b'
  return obj
}

といった感じに(ただし実際にはもう少し複雑な場合に)、とりあえず空のオブジェクトを作ってプロパティを後付けしたくなるときがあるが、それTypeScriptだとどうするのがいいんだろうと疑問に思った。

ながなが

とりあえず、返り値として想定しているのは次のような型

type FooBar = {
  foo: string
  bar: string
}

なので、関数の返り値の型注釈としては

function getObj(): FooBar {
  const obj = {}
  obj.foo = 'a'
  obj.bar = 'b'
  return obj
}

としたいところなのだが、objを宣言した時点で型が{}と推論されるので、次の2つの問題がある。

  1. foobarを追加できない
  2. return時にも{}型判定なことは変わらず、FooBar型を返していることにはならない
ながなが

1. foobarを追加できない

JSからTSに移行するときは一旦無視して、単純に一からTSで動的にプロパティをつけたいケースであれば、これは書き方を少し変えれば回避はできる。obj.(プロパティ)の形で存在しないプロパティにアクセスしているから怒られるのであって、次のような書き方であれば問題はない。

function getObj(): FooBar {
  let obj = {}
  obj = { ...obj, foo: 'a' }
  obj = { ...obj, bar: 'b' }
  return obj
}

{}型のobj{ foo: 'a' }のようなより詳細な型(※構造的部分型の考え方で互換性があるもの)を代入するのは問題ない。すごくざっくりしたことを言うとAnimal型の変数にDog型のオブジェクトを代入しても問題ないのと一緒。逆は当然ダメ。

ただし、この場合にもreturn時までずっとobj{}型の判定であることは変わらず2の問題は残っている。

ながなが

2. return時にも{}型判定なことは変わらず、FooBar型を返していることにはならない

これは結局、帰り値で返しているものが{}型であるという部分が問題なので、方針としては

  • 型アサーションを使ってobjの型を手動で変える
  • そもそもobjを使わない

の2パターンが思いつく。

前者の場合だと次のような形になる。

function getObj(): FooBar {
  const obj = {} as FooBar
  obj.foo = 'a'
  obj.bar = 'b'
  return obj
}

この場合、objの宣言時点でFooBar型の扱いになるので、1の問題も解決される。一番最初にあげたコードから型を付けたくらいで、処理は変わっていないのでJSからの移行時であれば最も手軽なような気がする。ただ、objを宣言した時点でFooBar扱いなので、objreturnする時点で必要なプロパティが揃っていることは保証されない(例えばobj.bar = 'b'の行がなかったとしてもエラーにはならない)という欠点がある。

後者の場合だと次のような形になる。

function getObj(): FooBar {
  const foo = 'a'
  const bar = 'b'
  return { foo, bar }
}

趣旨がずれている気もするが、結局foobarの値を取得するまでに、もう少し実際は複雑な段階があって、それをobjに各段階で追加していきたいというよう目的であれば、その目的自体は達成できている。当然objがないので1の問題も発生しない。

ながなが

結論

以下のようにするのが一番自然な気がする。

  • TSで一からコーディングするなら、{}のオブジェクトをとりあえず作っておくような書き方はせず、必要な情報が出揃ってからオブジェクトを作る。
  • JSからの移行なら型アサーションを使う。
ながなが

疑問...

もう少し空気を読んで型推論を更新してくれたりする方法はないのだろうか。

何も考えてないがこんな感じ

function getObj(): FooBar {
  const obj = {}
  obj.foo = 'a' // { foo: string } と型推論が更新される
  obj.bar = 'b' // { foo: string, bar: string } と型推論が更新される
  return obj
}

ただ、勝手に型推論が更新されると困るので、特定の注釈がある場合に更新するとか?ただ、型アサーションを各段階でフルで書くのはそれは面倒臭いのです。

良い方法とか知っている人いたら気軽にコメントください。