[TypeScript]オブジェクトのパスを指定して中身を取り出す方法 ~ ここは型の地獄 ~
TypeScript で再帰的な型を定義するを参考にしています
オブジェクトの中の型
以下のようなプログラムを組む際、中の値を取り出すだけならプログラム的には容易です。しかしTypeScriptの型を取り出す場合は一筋縄ではいきません。ということで値だけでなく、型もきちんと戻ってくるようにプログラムを作成しました。
//テスト用の型
const value01 = {
a: { b: { c: 100 } }
};
//データの取り出し(戻り値の型はnumber型)
const v = getValue(value01, ["a", "b", "c"] as const);
//100が出力される
console.log(v);
データの型と値を取り出すプログラム
//配列の二番目以降を取り出す取得
type Next<U> = U extends readonly [string, ...string[]]
? ((...args: U) => void) extends (top: any, ...args: infer T) => void
? T
: never
: never;
type Deeps<
State,
Paths extends readonly string[]
> = Paths[0] extends keyof State
? {
0: State[Paths[0]];
1: Deeps<State[Paths[0]], Next<Paths>>;
}[Paths[1] extends undefined ? 0 : 1]
: never;
//パスに沿ってオブジェクトの型を取り出す
function getValue<State extends object, Paths extends readonly string[]>(
state: State,
paths: Paths
) {
return (paths.reduce(
(state, path) => {
return state[path] as never;
},
state as { [key: string]: unknown }
) as unknown) as Deeps<State, Paths>;
}
解説
まず重要なのが値を渡すときに
const v = getValue(value01, ["a", "b", "c"] as const);
というように、「as const」を付けて、リテラルの配列を渡しているところです。リテラル以外で情報を渡しても、配列の中の型情報が "a" ではなく string に変換されてしまうため、オブジェクトのキーとして使えなくなってしまいます。また、渡した先にreadonly属性が付いていますが、constを渡す場合には必要となります。
次が以下のように疑似再帰を行っている部分です。これは冒頭の参照元を読んでください。経路切り替えをして、再帰しているのにしていないと誤魔化すテクニックのようです。ただし利用できる再帰深度は41までらしいので注意が必要です。ちなみにTypeScript3.7betaでも型の再帰は出来ませんでした。
{
0: State[Paths[0]];
1: Deeps<State[Paths[0]], Next<Paths>>;
}[Paths[1] extends undefined ? 0 : 1]
その他、genericsで使われるextendsの意味も知っておかなければなりません。
- 左辺のextends
型の制約を付けるために使用する - 右辺のextends
三項演算子的に型の判定を行うために使用する
これが分かっていないと、そもそもまともに型情報の管理が出来ません。しかしこれを真面目に書いていくと、型地獄へといざなわれます。
まとめ
Reduxを簡単に使うためのラッパーライブラリを作るのに、どうしても型情報の抽出が必要だったので、今回の内容にたどり着きました。
React-Reduxが難しい? それは過去の話だ! ~ ToDoアプリを最小限の労力で記述する ~
TypeScriptは多少知識を付けたと思っても、時々何をやっているんだかさっぱり分からないコードに遭遇することがあります。そういうものに遭遇したらイジり倒して理解していくしかありません。
TypeScriptのバージョンが上がって、初見で理解の出来ないコードが次々に登場するかと思うと、ワクワクが止まりません。