LoginSignup
7
6

【TypeScript】オブジェクトリテラルからUnion型を取得するジェネリック型をつくってみた

Posted at

導入

TypeScriptでenum(列挙型)を検索すると、非推奨の文字がちらつくかと思います。
実際にいつもお世話になっているサバイバルTypeScriptでは以下の様に記載されています。

列挙型はTypeScript独自すぎる

TypeScriptの列挙型に目を向けると、構文もJavaScriptに無いものであるだけでなく、コンパイル後の列挙型はJavaScriptのオブジェクトに変化したりと、型の世界の拡張からはみ出している独自機能になっています。TypeScriptプログラマーの中には、この点が受け入れられない人もいます。

数値列挙型には型安全上の問題がある

数値列挙型は、number型なら何でも代入できるという型安全上の問題点があります。次の例は、値が0と1のメンバーだけからなる列挙型ですが、実際にはそれ以外の数値を代入できてしまいます。

上記の通り、Enumは型安全上の観点から、積極的に利用しないほうが良さそうです。
そこで、代替案としてユニオン型オブジェクトリテラルを使う方法が、しばしば挙げられています。

例えば、Enumを以下のように定義したとします。
↓↓↓

enum FruitEnum {
  Apple  = "りんご",
  Banana = "ばなな",
}

const fruit1: FruitEnum = FruitEnum.Apple // コンパイルが通る
const fruit2: FruitEnum = "スターフルーツ" // エラーが出ます

上記Enumをユニオン型として定義すると、以下のようになります。
例えば、Enumの値に意味がない場合(ただの単語)に、こちらの記載がよく使われている印象です。
↓↓↓

type Fruits = "Apple" | "Banana"

const toJapanese(fruit: Fruits) {
  switch (fruit) {
    case "Apple":
      return "りんご"
    case "Banana":
      return "ばなな"
  }
}

同様にオブジェクトリテラルで定義した場合は以下のようになります。
こちらはEnumの値に意味がある場合に使われている印象です。
(調べた限りだとそんなニュアンスでしたが、厳密な使い分けはまだ理解できていません。。)
↓↓↓

const Fruits = {
  Apple: "りんご",
  Banana: "ばなな",
} as const

type Fruit = (typeof Fruits)[keyof typeof Fruits]

// ↑↑↑ type Fruit = "りんご" | "ばなな" ↑↑↑

const toJapanese(fruit: Fruits) {
  switch (fruit) {
    case Fruit.Apple:
      return "りんご"
    case Fruit.Banana:
      return "ばなな"
  }
}

しかし、オブジェクトリテラルをEnumの代替として使用する場合、毎回毎回以下のように定義してユニオン型を取得する必要が出てきてしまいます。

type Fruit = (typeof Fruits)[keyof typeof Fruits]

「これどうにかならないかな〜?」と思って、本題のオブジェクトリテラルからUnion型を取得できるジェネリック型を作ってみました。

(導入部分が長くてすみません。。)

結論

以下のようにジェネリック型(ObjectLiteralToUnion)として定義してあげることで、柔軟性・再利用性に富む共通関数っぽいものが作れます。

const Fruits = {
  Apple: "りんご",
  Banana: "ばなな",
} as const

type ObjectLiteralToUnion<T> = T extends { [K in keyof T]: infer U } ? U : never

type Fruits = ObjectLiteralToUnion<typeof Fruits>
// type Fruit = "りんご" | "ばなな"

また、型引数がシンプルなオブジェクトの場合には、以下のようにしても期待している動作になります。

type SimpleToUnion<T> = T[keyof T]

type Fruits = SimpleToUnion<typeof Fruits>
// type Fruit = "りんご" | "ばなな"

解説

まずはObjectLiteralToUnionを分解してみましょう。

1. 型引数Tの導入

Tは任意のオブジェクト型を表しています。

type ObjectLiteralToUnion<T> = ...

2. 条件型(extends

型引数Tが特定の形 ({ [K in keyof T]: infer U }) にマッチするかどうかを確認します。

T extends { [K in keyof T]: infer U } ? U : never

3. キーのループ ([K in keyof T]:)

keyof Tは型引数Tに含まれているキーをユニオン型として保持しています。
また、[K in keyof T]は、そのキー内でループし、各キーに対するプロパティの値を保持します。

4. 型推論 (infer U)

③で定義した型Kに対して、infer Uで各プロパティの型をUとして型推論します。
inferについてはこちらがわかりやすかったので参考までに。

5. 条件評価の結果

型引数T{ [K in keyof T]: infer U }の形にマッチした場合、U(Fruitsでいうところのりんごばなな)を返します。
マッチしない場合はneverを返します。

以上によって、各プロパティの値を持つUnion型を取得できるというわけです。

もっと簡潔な共通のジェネリクス型はないの?

実はあります。
導入や結論の部分でもちょくちょく出ていますが、

type Fruit = (typeof Fruits)[keyof typeof Fruits]

これを応用して汎用的なジェネリクス型として定義した場合、以下のようになります。
とてもシンプルですね!

type SimpleToUnion<T> = T[keyof T]

型引数Tがシンプルなオブジェクト(プロパティがstringだけのオブジェクトなど)では使えそうですね!
しかし、ちょっと困ったことが起きてしまいます。
例えば、オブジェクトTが複雑な型(ネストされた型やその他の型の組み合わせ)を含む場合、対応できないことがあります。
以下のようにオブジェクトがネストした場合ですと、2つのオブジェクト型(Citrus, Berry)を持つUnion型が返却されてしまいます。

const NestedFruits = {
  Citrus: {
    Orange: "オレンジ",
    Lemon: "レモン",
  },
  Berry: {
    Strawberry: "いちご",
    Blueberry: "ブルーベリー",
  },
} as const

type SimpleToUnion<T> = T[keyof T]

type NestedFruit = SimpleToUnion<typeof NestedFruits>
// type NestedFruit = { Orange: "オレンジ"; Lemon: "レモン"; } | { Strawberry: "いちご"; Blueberry: "ブルーベリー" }

対して、ObjectLiteralToUnionで対応するとどうでしょうか?
型引数のところでループをやっているだけなのですが、型引数にいれる型を柔軟に対応できるかと思います。

const NestedFruits = {
  Citrus: {
    Orange: "オレンジ",
    Lemon: "レモン",
  },
  Berry: {
    Strawberry: "いちご",
    Blueberry: "ブルーベリー",
  },
} as const

type ObjectLiteralToUnion<T> = T extends { [K in keyof T]: infer U } ? U : never

// ネストされたオブジェクト全体のプロパティ値を抽出
type NestedFruit = ObjectLiteralToUnion<{
  [K in keyof typeof NestedFruits]: ObjectLiteralToUnion<typeof NestedFruits[K]>;
}>
// type NestedFruit = "オレンジ" | "レモン" | "いちご" | "ブルーベリー"

まとめ

以上のように、オブジェクトリテラルからUnion型を変換する場合は「型引数が単純かどうか?」で使い分けて使用すると良さそうです。

型引数がシンプルなオブジェクトの場合

type SimpleToUnion<T> = T[keyof T]

型引数が複雑なオブジェクトの場合

type ObjectLiteralToUnion<T> = T extends { [K in keyof T]: infer U } ? U : never

長々と書きましたが、オブジェクトリテラルからUnion型に変換したい場面は往々にしてあると思います。
そのような場合は、本記事を参考にしていただけると非常に嬉しいです!
(間違っているところがありましたら、コメントしていただけると助かります。。。)

ではでは!

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6