Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

access(obj, 'foo.bar.baz') みたいにパスを指定してアクセスしたい

発端: ApolloClient の useQuery の data と error をいい感じに扱うために getOrThrow(data, error, 'path.to.field') みたいなユーティリティ関数が欲しくなった

欲しい関数

const {loading, error, data} = useQuery(query);
const fooBar = getOrThrow(data, error, 'foo.bar');
const fooBarBaz = getOrThrow(data, error, 'foo.bar.baz');

こんな感じで、foo.bar でエラーが起きてたらエラーが throw され、エラーがなければ data.foo.bar が返ってくるという関数です。

実装:

さっそく実装を載せます(throw までつけると長くなるので、get に特化した実装を載せてます)。

ちなみに type-challenges の Object Key Paths の解答コードを借りました (こちらの解答)。

// cf. https://github.com/type-challenges/type-challenges/issues/7939
type ObjectKeyPaths<
  T extends object,
  P extends string = '', // prefix
  K extends keyof T = keyof T,
> =
  K extends string ?
    | `${P}${K}`
    | (T[K] extends object ? `${P}${K}${ObjectKeyPaths<T[K], '.'>}` : never)
  : never;

type Access<T extends object, U> =
  U extends `${infer x extends keyof T & string}.${infer xs}` ?
    T[x] extends object ?
      Access<T[x], xs>
    : T[x]
  : U extends keyof T ? T[U]
  : never;

const access = <T extends object, U extends ObjectKeyPaths<T>>(
  object: T,
  path: U,
): Access<T, U> =>
  path.split('.').reduce((acc, name) => (acc as any)[name], object) as any;

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript

型を無理くりつけてるため実装に any を使ってます。これはもう仕方ないので、テストで保証すれば OK って方針にしました。