🏭

as constを使って値からUnion型を生成する

2022/07/11に公開

TypeScriptでは、
型を定義する → それに合わせて値を定義する
のが一般的な型定義の方法です。

しかし、以下のように定数とそのUnion型を定義したい場合はどうでしょうか?
新たにプロパティを足す度に型定義を修正する必要があり面倒です。

type SettingKeys = "foo" | "bar" | "baz"
type SettingValues = 1 | 2 | 3
const SETTING: Record<SettingKeys, SettingValues> = {
  foo: 1,
  bar: 2,
  baz: 3,
  // ↓を足そうとするとSettingKeys、SettingValuesの型定義を修正する必要がある
  // piyo: 4
}

こういった場合は逆に
値を定義する → 値から型が自動的に定義される
となると良さそうです。

type SettingKeys = Keys<typeof SETTING>
type SettingValues = Values<typeof SETTING>
const SETTING: Record<SettingKeys, SettingValues> = {
  foo: 1,
  bar: 2,
  baz: 3,
  piyo: 4 // ←型の修正をせずにプロパティを足せる
}

そこで、本記事ではas constを使って、定数の値からUnion型を生成する方法についてご紹介します。

DR;TL;

急いでいる方は以下の型定義をご利用ください。

オブジェクトの値からUnion型を生成する

/** 
オブジェクトの値からUnion型を生成する
@example
const Hoge = {
  a: 1,
  b: 2,
  c: 3
} as const

type HogeKeys = Keys<typeof Hoge> // => 'a' | 'b' | 'c'
type HogeValues = Values<typeof Hoge> // => 1 | 2 | 3
 */
type Keys<T> = keyof T
type Values<T> = T[Keys<T>]

配列の値からUnion型を生成する

const array = ["a", "b", "c"] as const
// "a" | "b" | "c"
type Values = typeof array[number]

as constとは

as constはTypeScript 3.4で導入されたconst assertionを行うための記法です。

式の後ろas const をつけることで、型推論に以下の効果を及ぼします。

  1. Widening*されない
  2. オブジェクトリテラルのプロパティはreadonlyになる
  3. 配列リテラルはredonlyのタプル型になる。
// 1. Wideningされない
// "hello"型
let x = "hello" as const

// 2. オブジェクトリテラルのプロパティはreadonlyになる
// '{ readonly text: "hello" }'型
let z = { text: "hello" } as const;

// 3. 配列リテラルはredonlyのタプル型になる。
let z = [10, 20] as const;

💡Wideningとは?

Widening(Wideの動名詞)とはリテラル型が自動的に対応するプリミティブ型に広げられて推論されることを指します。
以下の例では同じ”hello”という値が代入されていますが、letで宣言されたyは”hello”型ではなくstring型として推論されています。

// "hello"
const x = "hello"
// string (Wideningされてプリミティブ型のstringとなる)
let y = "hello"

これはletで宣言された変数は後で再代入されることが期待されているため、”hello” 型のプリミティブ型のstringとして推論されるためです。

オブジェクトの場合も代入されることを期待しているので、リテラル型ではなくプリミティブ型として推論されます。

const obj = {
  foo: "hoge", // string型
  bar: 1,      // number型
  piyo: true,  // boolean型
}

このように再代入されることを期待して、型を広げて推論されることをWideningと言います。


オブジェクトの値からUnion型を生成する

as constが理解できたところで、オブジェクトの値からUnion型を定義します。

こちらのSETTINGという定数のキーのUnion型(SettingKeys型)と値のUnion型(SettingValues)型を値から生成します。

const SETTING = {
  foo: 1,
  bar: 2,
  baz: 3,
}

キーのUnion型(SettingKeys型)

キーのUnion型は簡単ですね、typeofでSETTINGを値から型に変換し、そこからkeyofでキーのUnion型を取得できます。

// "foo" | "bar" | "baz" 型
type SettingKeys = keyof typeof SETTING

値のUnion型(SettingValues)

続いて値のUnion型を定義します。先ほどのSettingKeysの型定義を使って、以下のように書けば値のUnion型が取得できそうです。

type SettingValues = typeof SETTING[SettingKeys]

チェックしてみましょう!
あら?欲しいのは 1 | 2 | 3型なのにnumber型になってしまっています🤔
1.png

勘の良い方はもうお気づきですよね?
これはSETTINGの値がWideningされたためにnumber型として、推論されてしまっているためです。
2.png

そうです!ここでas constの出番です!
SETTINGにas constをつけることで、WideningされずにSettingValuesに期待していた1 | 2 | 3 型が入ります。

見事に値からUnion型を生成することができました👏

const SETTING = {
  foo: 1,
  bar: 2,
  baz: 3,
} as const // as constをつけることでWideningされないように✅

type SettingKeys = keyof typeof SETTING
// 1 | 2 | 3 型
type SettingValues = typeof SETTING[SettingKeys]

抽象化

他の定数にも利用できるように抽象化しておきます。

type Keys<T> = keyof T
type Values<T> = T[Keys<T>]

他の値もシンプルにUnion型が生成できるようになりました💪

利用時に

  • 値にas constをつけないといけない
  • 型引数をtypeofで渡す必要がある

点に注意が必要です。

const Hoge = {
  a: 1,
  b: 2,
  c: 3
} as const

type HogeKeys = Keys<typeof Hoge> // => 'a' | 'b' | 'c'
type HogeValues = Values<typeof Hoge> // => 1 | 2 | 3

配列の値からUnion型を生成する

続いて、配列の値からUnion型を生成してみましょう。
以下の配列から ‘a’ | ‘b’ | ‘c’ のUnion型を生成することを目指します。

const array = ['a', 'b', 'c']

といっても、先ほどと同じ要領で以下のように書くだけです。

const array = ["a", "b", "c"] as const
// "a" | "b" | "c"
type Values = typeof array[number]

まず、オブジェクトの時と同様にas const をつけることで、arrayは読み取り専用のタプル型となります。

// readonly ["a", "b", "c"]
const array = ["a", "b", "c"] as const

さらに配列はTypeScriptではnumber型をキーにもつオブジェクトなので、このarray の値からnumber型のインデックスの値の型を取得することで、配列の値のUnion型が生成できるわけです。

// "a" | "b" | "c"
type Values = typeof array[number]

以上やりたくなるタイミングがときどきあると思うので、as const 活用してみてください👍

参考
https://stackoverflow.com/questions/50044023/get-union-type-from-indexed-object-values

ベリー本🫐
https://amzn.to/3as73jf

Discussion