🌵

ネストされたオブジェクトからリテラル型のユニオン型を作成する

2022/07/16に公開約4,100字

やりたいこと

const FruitMap = {
	APPLE: {value: 'apple', price: 100},
	BANANA: {value: 'banana', price: 200},
	MANGO: {value: 'mango', price: 300},
}

このようなネストされたオブジェクトから

type Price = 100 | 200 | 300

こういう型を生成したい🤨

結論

const FruitMap = {
	APPLE: {value: 'apple', price: 100},
	BANANA: {value: 'banana', price: 200},
	MANGO: {value: 'mango', price: 300},
} as const;

type FruitMapKey = keyof typeof FruitMap

type Price = typeof FruitMap[FruitMapKey]['price']

必要な材料

この型を作るために必要なTypeScriptの知識は4つあります。

  • as const
  • typeofキーワード
  • keyof型
  • lookup型

as const

as constを付けることによって、

・オブジェクトリテラルから推測されるオブジェクト型は全てのプロパティがreadonlyになる
・文字列や数値などに対して付けられるリテラル型がwideningしないリテラル型になる

という効果があります[1][2]

さっそく見ていきましょう!
as constを用いる前のFruitMapの型を見ると、以下のようになっています。

FruitMapの型
const FruitMap: {
    APPLE: {
        value: string;
        price: number;
    };
    BANANA: {
        value: string;
        price: number;
    };
    MANGO: {
        value: string;
        price: number;
    };
}

value の型が stringprice の型が number にそれぞれwideningされています。

では、 as const をつけた FruitMap の型を確認しましょう。VS codeやcodesandboxで FruitMap にhoverすると型情報が表示されるかと思います。

FruitMapの型(as constをつけた場合)
const FruitMap: {
    readonly APPLE: {
        readonly value: "apple";
        readonly price: 100;
    };
    readonly BANANA: {
        readonly value: "banana";
        readonly price: 200;
    };
		readonly MANGO: {
        readonly value: "mango";
        readonly price: 300;
    };
}

このように、as constを付けることによって、各プロパティにreadonlyがついていることと、値がnumberやstringにwideningされずにリテラル型になっていることが確認できます。

typeofキーワード

typeofキーワードは、型を作るキーワードです。 typeof 変数 で変数の型を抽出します。
FruitMap に対してtypeofキーワードをかけた場合は、下記のようになります。

type FruitMapType = typeof FruitMap
FruitMapTypeの型
{
    readonly APPLE: {
        readonly value: "apple";
        readonly price: 100;
    };
    readonly BANANA: {
        readonly value: "banana";
        readonly price: 200;
    };
		readonly MANGO: {
        readonly value: "mango";
        readonly price: 300;
    };
}

as constのときに確認した FruitMapの型と一致していることがわかります。このように、変数に対してtypeofキーワードを用いることでその変数の型を抽出し、別の型として代入することができます。

keyof型

オブジェクト型からそのオブジェクトのプロパティ名(key)の型を取得する機能です。

先ほどtypeofキーワードを使用したことによって、オブジェクト型 FruitMapType が取得できました。FruitMapType をもう一度確認すると以下のようになっていました。

FruitMapTypeの型
{
    readonly APPLE: {
        readonly value: "apple";
        readonly price: 100;
    };
    readonly BANANA: {
        readonly value: "banana";
        readonly price: 200;
    };
    readonly MANGO: {
        readonly value: "mango";
        readonly price: 300;
    };
}

keyof型と組み合わせることにより、プロパティ名(key)の文字列をユニオン型で取得することができます。

type FruitMapKey = keyof FruitMapType
// FruitMapKey -> "APPLE" | "BANANA" | "MANGO

keyof typeof

冒頭で記載した「結論」では、typeofとkeyofを1行でまとめて行なっています。

type FruitMapKey = keyof typeof FruitMap
// FruitMapKey -> "APPLE" | "BANANA" | "MANGO

keyof typeof 変数 というパターンは、オブジェクト型の変数のプロパティ名を文字列のユニオン型で渡すことができるため、よく使われています。覚えましょう(自戒)。

lookup型

lookup型を用いることで、オブジェクトの値取得のような感覚で型情報を取り出して再利用することができます。文章だけだと何のこっちゃ感がすごいので、例を載せます。[3]

lookup型の使用例
type Cat = {
  name: string,
  breed: string,
  age: number,
}

// Nameはstringになる
type Name = Cat['name']

lookup型を用いることによって、オブジェクト型のvalueの型を取得することができます。
さっそく、FruitMap型の値の方の型を取得してみましょう。

typeof FruitMap[FruitMapKey]
// FruitMapKey -> "APPLE" | "BANANA" | "MANGO"
// typeof FruitMap[FruitMapKey] の型↓
{
  readonly value: "apple";
  readonly price: 100;
} | {
  readonly value: "banana";
  readonly price: 200;
} | {
  readonly value: "mango";
  readonly price: 300;
}

取得できた方型はオブジェクト型のユニオン型ですので、さらにlookup型を用いることができます。
もう1度lookup型を用いて、priceを取得しましょう!

type Price = typeof FruitMap2[FruitMapKey]['price']
// Price -> 100 | 200 | 300

完成!おつかれさまでした!

脚注
  1. 引用文献:プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで ↩︎

  2. 他にも効果はありますが、今回注目したい効果はこの2つです ↩︎

  3. すっごい周りくどい例ですが、こういう使い方ですよということで… ↩︎

Discussion

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