Closed8

ネストしたオブジェクトの値を取り出す型安全な関数

ピン留めされたアイテム
Yunosuke YamadaYunosuke Yamada

最終的な成果物

type KeyOf<T> = keyof T & string;
type ObjectPath<T, K extends KeyOf<T>> = K extends K // @ts-ignore
  ? `${K}.${Path<T[K]>}`
  : never;

type IsFinite<T extends any[]> = number extends T["length"] ? false : true;
type IsTuple<T> = T extends any[] ? IsFinite<T> : false;
type IsArray<T> = T extends any[]
  ? IsFinite<T> extends true
    ? false
    : true
  : false;

type TupleKeys<T extends any[]> = Exclude<keyof T, keyof any[]> & string;
type ArrayKey = `${number}`;

type TuplePath<T extends any[], K extends TupleKeys<T>> = K extends K
  ? `[${K}].${Path<T[K]>}`
  : never;
type ArrayPath<T extends any[]> = `[${ArrayKey}].${Path<T[number]>}`;

type Path<T> = T extends any[]
  ? IsTuple<T> extends true
    ? `[${TupleKeys<T>}]` | TuplePath<T, TupleKeys<T>>
    : IsArray<T> extends true
    ? `[${ArrayKey}]` | ArrayPath<T>
    : never
  : T extends object
  ? KeyOf<T> | ObjectPath<T, KeyOf<T>>
  : never;

type ObjectValue<T, K> = K extends keyof T ? T[K] : never;
type TupleValue<T, K> = K extends `[${infer L}]`
  ? L extends keyof T
    ? T[L]
    : never
  : never;
type ArrayValue<T, K> = K extends `[${number}]`
  ? T extends any[]
    ? T[number]
    : never
  : never;
type NestedValue<T, K> = IsTuple<T> extends true
  ? TupleValue<T, K>
  : IsArray<T> extends true
  ? ArrayValue<T, K>
  : T extends object
  ? ObjectValue<T, K>
  : never;

type Value<T, P> = P extends `${infer K}.${infer Rest}`
  ? Value<NestedValue<T, K>, Rest>
  : NestedValue<T, P>;

const getValue = <T, P extends Path<T>>(x: T, path: P): Value<T, P> => {
  return path.split(".").reduce((value: any, key) => {
    if (!value) {
      return undefined;
    }
    const index = key.match(/^\[(\d+)\]$/);
    if (index) {
      return value[parseInt(index[1])];
    }
    return value[key];
  }, x);
};
Yunosuke YamadaYunosuke Yamada

オブジェクトのパスを取得する型
配列、タプルは一旦考えない

type KeyOf<T> = keyof T & string;
type ObjectPath<T, K extends KeyOf<T>> = K extends K
  ? `${K}.${Path<T[K]>}`
  : never;
type Path<T> = T extends object ? KeyOf<T> | ObjectPath<T, KeyOf<T>> : never;
Yunosuke YamadaYunosuke Yamada

オブジェクトとパスを指定して値を取得する型

type Value<T, P> = P extends `${infer K}.${infer Rest}`
  ? K extends keyof T
    ? Value<T[K], Rest>
    : never
  : P extends keyof T
  ? T[P]
  : never;
Yunosuke YamadaYunosuke Yamada

実装

const getPathValue = <T, P extends Path<T>>(x: T, path: P): Value<T, P> => {
  return path
    .split(".")
    .reduce((value: any, key) => (value ? value[key] : undefined), x);
};
Yunosuke YamadaYunosuke Yamada

配列とタプルにも対応しよう

まずはユーティリティ

type IsFinite<T extends any[]> = number extends T["length"] ? false : true;
type IsTuple<T> = T extends any[] ? IsFinite<T> : false;
type IsArray<T> = T extends any[]
  ? IsFinite<T> extends true
    ? false
    : true
  : false;

type TupleKeys<T extends any[]> = Exclude<keyof T, keyof any[]> & string;
type ArrayKey = `${number}`;
Yunosuke YamadaYunosuke Yamada

ObjectPathに相当するTuplePath, ArrayPathを定義し、Pathを再定義する

type TuplePath<T extends any[], K extends TupleKeys<T>> = K extends K
  ? `[${K}].${Path<T[K]>}`
  : never;
type ArrayPath<T extends any[]> = `[${ArrayKey}].${Path<T[number]>}`;

type Path<T> = T extends any[]
  ? IsTuple<T> extends true
    ? `[${TupleKeys<T>}]` | TuplePath<T, TupleKeys<T>>
    : IsArray<T> extends true
    ? `[${ArrayKey}]` | ArrayPath<T>
    : never
  : T extends object
  ? KeyOf<T> | ObjectPath<T, KeyOf<T>>
  : never;
Yunosuke YamadaYunosuke Yamada

Valueについてもタプルと配列を考慮する

type ObjectValue<T, K> = K extends keyof T ? T[K] : never;
type TupleValue<T, K> = K extends `[${infer L}]`
  ? L extends keyof T
    ? T[L]
    : never
  : never;
type ArrayValue<T, K> = K extends `[${number}]`
  ? T extends any[]
    ? T[number]
    : never
  : never;
type NestedValue<T, K> = IsTuple<T> extends true
  ? TupleValue<T, K>
  : IsArray<T> extends true
  ? ArrayValue<T, K>
  : T extends object
  ? ObjectValue<T, K>
  : never;

type Value<T, P> = P extends `${infer K}.${infer Rest}`
  ? Value<NestedValue<T, K>, Rest>
  : NestedValue<T, P>;
Yunosuke YamadaYunosuke Yamada

タプル、配列も考慮した実装

const getValue = <T, P extends Path<T>>(x: T, path: P): Value<T, P> => {
  return path.split(".").reduce((value: any, key) => {
    if (!value) {
      return undefined;
    }
    const index = key.match(/^\[(\d+)\]$/);
    if (index) {
      return value[parseInt(index[1])];
    }
    return value[key];
  }, x);
};
このスクラップは2024/01/14にクローズされました