TypeScriptで深いJSON構造から要素を取り出すときに型をちゃんと取るTIPS
Contents
下記のような多段のマスターデータが存在しているときに、ちゃんと型安全に値を取り出したいとなったときの型定義を考える
large.json
| |
detail.json
| |
これに対して、下記のような参照をしたら、ちゃんと値を取りたい。
| |
※ちなみに上記で import している large, detail は as const したような厳密な型定義になっていることを前提とする。
| |
Step1: 深さ指定して key の型を取れるようにする
https://qiita.com/KuwaK/items/587205867d333b705a41 を参考にさせていただき、下記のような型を作成する(Property2 を NthDepthProperty に変えているが、必要であればもとに戻します)
| |
簡単に説明すると、最大 3 段ネストするオブジェクトに対して、型引数 2 つ目に指定した階層に存在する key を Union Type にして返却するというもの。NumMap を増やせば当然いくらでもネストは可能だが、今回は 3 段のままで大丈夫なのでそのままにする
その上で元々のファイルを下記のように変えると、引数として望まれる型が適切に指定できる
| |
largeKey は keyof でもいいんだけど、見た目的に揃えた
Step2: Union Type を Intersection に変える型を用意する
上記のままでも行けそうな感じはするが、下記のようなエラーが出てしまう
1 2 3 4 5 6 | (parameter) detailKey: ValueOf<{
1: "1001" | "1002";
2: "2001" | "2002";
}>
型 'ValueOf<{ 1: "1001" | "1002"; 2: "2001" | "2002"; }>' の式を使用して型 '{ "1001": "foo1"; "1002": "foo2"; } | { "2001": "baz1"; "2002": "baz2"; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
プロパティ '1001' は型 '{ "1001": "foo1"; "1002": "foo2"; } | { "2001": "baz1"; "2002": "baz2"; }' に存在しません。 |
これはdetail[largeKey] で取得した値の型が
1 2 3 4 5 6 7 | {
"1001": "foo1";
"1002": "foo2";
} | {
"2001": "baz1";
"2002": "baz2";
} |
となっており、例えばこのオブジェクトに対して”1001”という key を指定した場合、largeKey=2 で参照すると出てくる後方の型にマッチしない可能性があるためエラーになるかもしれないから。
回避策としては、下記のように丁寧に TypeGuard をして行くことも考えられるが、key が増えるたびに処理が増えてしまい、なんのための型定義をしているのかよくわからなくなってしまう。
| |
ここで開発者は事前に largeKey と detailKey の組み合わせがほぼ確実に正しい形で渡って生きていることを知っている(そうでない場合は一旦想定しないで OK)とした場合、Union 型を Intersection 型に変える事によって解決する。 つまり
1 2 3 4 5 6 7 | {
"1001": "foo1";
"1002": "foo2";
} | {
"2001": "baz1";
"2002": "baz2";
} |
を
1 2 3 4 5 6 7 | {
"1001": "foo1";
"1002": "foo2";
} & {
"2001": "baz1";
"2002": "baz2";
} |
に変えてしまえば、”1001”でアクセスしたとしても確実に値を得られるということになる。
ここで、Intersection に変換する方法はこちらを参考に下記のような型で解決できる
| |
あとはこんな感じでコードを変更すれば hoge がいい感じの型で取得できるようになる
| |
おしまい
Author kotamat
LastMod 2020-12-30 (7e747ca)