◆結
前提として、型安全で引いてくるのは限度がある。
例えばあるオブジェクトの”piyo.a”の値をとりたいときは以下の感じ。
const a = { hoge: "", piyo: { a: "text", b: 100, }, }; const getValue = (s: string, item: object) => s.split(".").reduce<unknown>((p, c) => p?.[c as keyof typeof p], item); const b = getValue("piyo.a", a);
この形を取らざるを得なくなったら設計を見直したほうがいい。何かおかしいことをやろうとすると、こういったおかしいコードを書かされるはめになる。
reduceの型パラメーターは「unknown」にしている。だって何が取れてくるのかわからないから。使うときにasしてあげてね。
◆いきさつ
プロパティhogeやらプロパティpiyoを有するaってobjectがあったとして、プロパティに設定された値をとってくる方法はふたつある。
const a = { hoge: "", piyo: { a: "text", b: 100, }, }; // b と c は同じ const b = a.hoge; const c = a["hoge"]; // d と e は同じ const d = a.piyo.a; const e = a["piyo"]["a"];
a.hogeが「ドット表記法」、a[“hoge”]が「ブラケット表記法」。
TypeScriptを使ってると存在しないプロパティにアクセスしようとしたときとか注意してくれて安全だから、基本的にはすべてドット表記法によりプロパティアクセスするべきである。が、のっぴきならない事情によりブラケット表記法を使用することも稀にある。
ちなみに、a[“piyo”][“a”]はstringと型推論してくれる。かしこい。
a[“aaaaa”][“a”]だとany扱いになっちゃうね。
ブラケット表記法は文字列指定でプロパティにアクセスしてるわけだけど、以下の書き方は許されない。
const e = a["piyo.a"];
これ微妙につらい。ブラケット表記法を使わざるを得ないような状況に於いては、これをやりたくなるケースもそれなりにある。というので、最初に書いたgetValueを書いてみた。
◆as keyof typeof ってなんだよ
これ無くてもいいです。eslintくんを宥めるために書いてある。
eslint-plugin-securityっていうセキュリティチェックのプラグインを入れてると、インジェクションあるでって注意されるのね。
型 ‘string’ の式を使用して型 ‘{}’ にインデックスを付けることはできないため、要素は暗黙的に ‘any’ 型になります。
型 ‘string’ のパラメーターを持つインデックス シグネチャが型 ‘{}’ に見つかりませんでした。ts(7053)
Generic Object Injection Sinkeslintsecurity/detect-object-injection
eslint-plugin-security/the-dangers-of-square-bracket-notation.md at main · nodesecurity/eslint-plugin-security (github.com)
実際のところ放置したら危ない場合もあるんで見てあげたほうがいい。可能な限りドット表記法でアクセスするべきだが、見てあげたうえで使用法が安全だと思えるんならkeyof typeofで回避してよい。
▼typeof
オブジェクトのtypeが取れてくる。
TypeScriptの
typeof型演算子 | TypeScript入門『サバイバルTypeScript』 (typescriptbook.jp)typeof
は変数から型を抽出する型演算子です。次は、変数point
にtypeof
演算子を用いて、Point
型を定義する例です。このPoint型は次のような型になります。
const point = { x: 135, y: 35 }; type Point = typeof point;
このとき、type Pointは
type Point = { x: number; y: number; }
こうなってる。
▼keyof
keyof型演算子 | TypeScript入門『サバイバルTypeScript』 (typescriptbook.jp)
keyof
はオブジェクト型からプロパティ名を型として返す型演算子です。たとえば、name
プロパティを持つ型に対して、keyof
を使うと文字列リテラル型の"name"
が得られます。
typeに対してkeyofを使用すると、各キーをユニオンした型が取れてくる。
type Book = { title: string; price: number; rating: number; }; type BookKey = keyof Book; // 上は次と同じ意味になる type BookKey = "title" | "price" | "rating";
プロパティ名を特定できるんすね。
keyofでとってきたUnion typeを使ってプロパティにアクセスすれば、プロパティの存在が保証されてるから安全。ランタイムエラーにならない。
ただし、今回の例ではasで無理やり解釈させてるから安全にはなってない。存在するプロパティ名でプロパティを引いてきてますよっていうポーズとしての「as」です。
◆解説
const getValue = (s: string, item: object) => s.split(".").reduce((p, c) => p?.[c as keyof typeof p], item) as unknown;
s.split(“.”)により “piyo.a” を [“piyo”, “a”] って配列に砕く。
▼reduce
いわゆる畳み込みと呼ばれる処理。
reduceは配列に対して使用し、二つのパラメータを受け取る。
第二引数のitemが初期値。
第一引数のfunctionを配列の各要素ごとに実行するわけだが、mapとかfilterとは違ってふたつの引数をとるfunctionであるな。それぞれ何なんだろか。例になぞらえて説明する。
- p
- 「前回の処理からの繰り越し結果」を意味している。
- 繰り越しのない一週目では初期値、すなわちreduceの第二引数がそのまま取れてくる。
- c
- 配列の各要素。
- mapとかfilterで取れてくるやつと同じ。
pは「previous」でcは「current」のこと。
1から10までの数字を足す処理
を書こうと思ったら
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce((p, c) => p + c, 0);
こうっすね。
reduceの戻り値の型は基本的に初期値と同じ型と推論されるんだけど、<unknown>とか設定すればunknownにもなる。が、初期値と互換性のない型はパラメーターとして指定できない。<string>とか指定すると、objectと互換性がないから怒られます。
定義は以下の感じ。
TypeScript/lib.es5.d.ts at main · microsoft/TypeScript (github.com)
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
▼?.[]ってなに
オプショナルチェーン (?.) – JavaScript | MDN (mozilla.org)
これ。
ブラケット表記法でアクセスしたい && オプショナルチェーン使いたい場合は、?.[]って書き方になる。
存在しないプロパティにアクセスしようとしたときはundefinedが返ってくるようにしたかった。
◆結(にかいめ)
上記を駆使したら、ドットで区切られたプロパティ名によりオブジェクトからプロパティを引いてくることができるんすね。面倒だけど。
eslintはムカつくけど、気づかなかった作法やら懸念事項に気づく機会になってるんで助かるっちゃ助かる。導入はお早めに。
コメントを残す