◆結

前提として、型安全で引いてくるのは限度がある。

例えばあるオブジェクトの"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は変数から型を抽出する型演算子です。次は、変数pointtypeof演算子を用いて、Point型を定義する例です。このPoint型は次のような型になります。

typeof型演算子 | TypeScript入門『サバイバルTypeScript』 (typescriptbook.jp)
const point = { x: 135, y: 35 };
type Point = typeof point;

このとき、type Pointは

type Point = {
  x: number;
  y: number;
}

こうなってる。

▼keyof

keyofはオブジェクト型からプロパティ名を型として返す型演算子です。たとえば、nameプロパティを持つ型に対して、keyofを使うと文字列リテラル型の"name"が得られます。

keyof型演算子 | TypeScript入門『サバイバルTypeScript』 (typescriptbook.jp)

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はムカつくけど、気づかなかった作法やら懸念事項に気づく機会になってるんで助かるっちゃ助かる。導入はお早めに。