Open
4

TSの知見(自分が思い出す用)

コンストラクタで初期化するときのオプションの中で特定のプロパティのみをクラス全体の型にしたい場合のTIPS


interface HogeOptions<S> {
  fuga: S;
  bar: Bar<S>;
}
class Hoge<S, T extends HogeOption<S>> {
  constructor(option: T);
}

上記の様な定義があったときに、普通に下記のように呼び出しちゃうと第一型引数がunknownになってしまう。

new Hoge({
  fuga: "aaaa"
  bar: "aaaa"
})

理由としてはTの定義だけではSを推論できないため
これを後方互換を保ちつつSの型を推論するには、仮型パラメータを用意するのがいい

class Hoge<
  _,
  T extends HogeOption<_>,
  S = T["fuga"] extends _ ? T["fuga"] : _ 
> {
  constructor(option: T);
}

こうすると、Hogeの第一引数に型を定義した場合は _ がそのまま S にアサインされ、constructorで型付きで指定した場合はその引数のパラメータから型推論してSがアサインされる形になる

MappedTypeを使った、Vuex風のオブジェクト平坦化の方法

const s = {
  modules: {
    foo: {
      modules: {
        hoge: {
          actions: {
            bar: (n: number) => "aaa",
          },
        },
      },
    },
    bar: {
      actions: {
        bal: (x: string) => "aaa",
      },
    },
  },
};

みたいなのがあったときに

{
  "bar/bal": (x: string) => string,
  "foo/hoge/bar": (n: number) => string
}

みたいに変換したいときのTIPS。

ここで必要になるのは下記3点

  • Conditional Typesを使った、再帰的な展開
  • MappedTypesのKey Remappingを使った、Keyの文字列結合
  • UnionからIntersectionに変換する型
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;


type ExtractObject<
  T,
  S extends string = "actions",
  P extends string = ""
> = (T extends {
  [_ in S]: infer O;
}
  ? { [K in keyof O as `${P}${string & K}`]: O[K] }
  : {}) &
  (T extends {
    modules: infer O;
  }
    ? UnionToIntersection<
        {
          [K in keyof O]: ExtractObject<O[K], S, `${P}${string & K}/`>;
        }[keyof O]
      >
    : {});

を作ると平坦化できる。

おお良かったです!解説しようと思ったものの、書いてたらめんどくなってしまった…

ログインするとコメントできます