TypeScript でネストされたオブジェクト型の書き換え

↓ に対するアンサーソングです.

blog.3qe.us

例えばこういう感じの型 T があって, ネストされた内側にある baz の型を number から string に書き換えたいとしますね.

type T = {
  foo: {
    bar: {
      baz: number,
    },
  },
};

もしこれが, 書き換える対象のパスを ["foo", "bar", "baz"] のようなタプル型で表現して, Rewrite<T, ["foo", "bar", "baz"], string> のように書けたら素敵だと思いませんか? 私は思います.

というわけでやっていきましょう. まずはタプル型に対する head / tail を用意します.

type Head<XS extends readonly unknown[]> =
  XS extends [] ? never : XS[0];

type Tail<XS extends readonly unknown[]> =
  XS extends [] ? never : TailSub<XS>;

type TailSub<XS extends readonly unknown[]> =
  ((...xs: XS) => unknown) extends ((y: never, ...ys: infer YS) => unknown) ? YS : never;

TailSub がややトリッキーですね. 動作を確認してみましょう.

type Head0 = Head<[]>;                    // = never
type Head1 = Head<["foo"]>;               // = "foo"
type Head2 = Head<["foo", "bar", "baz"]>; // = "foo"

type Tail0 = Tail<[]>;                    // = never
type Tail1 = Tail<["foo"]>;               // = []
type Tail2 = Tail<["foo", "bar", "baz"]>; // = ["bar", "baz"]

これがあればタプル型で表現したパスをたどっていくことができそうです. 実際できて, 最終的な Rewrite の形は以下のようになります.

type Key = string | number | symbol;

type Rewrite<T, Path extends readonly Key[], A> =
  Path extends [] ? A : RewriteSub<T, Head<Path>, Tail<Path>, A>;

type RewriteSub<T, P extends Key, PS extends readonly Key[], A> = {
  [K in keyof T]: K extends P ? Rewrite<T[K], PS, A> : T[K]
};

これはほとんどやるだけ. 再帰は許されるパターンと許されないパターンがあるのでそこだけ注意が必要ですが.

ためしに動かしてみましょう.

type T = {
  foo: {
    bar: {
      baz: number,
    },
  },
};

type ModT = Rewrite<T, ["foo", "bar", "baz"], string>;
// 以下と同じ:
// type ModT = {
//   foo: {
//     bar: {
//       baz: string,
//     },
//   },
// };

const modT: ModT = {
  foo: {
    bar: {
      baz: "hello",
    },
  },
};

最後の文で型エラーが出ないということは無事成功です. よかったですね.

ちなみにもうちょっと面白いことも出来て, パスにユニオン型を混ぜることで, 複数のパスを同時に書き換えられます.

type U = {
  foo: {
    bar: {
      baz: number,
    },
  },
  qux: {
    bar: {
      baz: boolean,
    },
  },
};

type ModU = Rewrite<U, ["foo" | "qux", "bar", "baz"], string>;
// 以下と同じ:
// type ModU = {
//   foo: {
//     bar: {
//       baz: string,
//     },
//   },
//   qux: {
//     bar: {
//       baz: string,
//     },
//   },
// };

const modU: ModU = {
  foo: {
    bar: {
      baz: "hello",
    },
  },
  qux: {
    bar: {
      baz: "bye",
    },
  },
};

ここまでのコード全文はこちら. 現場からは以上です.