導入
今日の話題はTypeScriptで型定義を再利用してインターフェースの一部の任意プロパティを必須プロパティに変更したりする方法についてです。この逆の一部の必須プロパティを任意プロパティに変更するのはここで紹介する方法の応用なので省略します。
※ 私が株式会社愛宕 Advent Calendar 2023に書く記事は主に社内向けに共有しておきたいけど勉強会をするまでもないちょっとしたTipsにしたいと思います。
ここで、必須プロパティとは次の例のように定義されたインターフェース内のname
やtype
で、任意プロパティとはage
やhomeland
を指します。
interface Player {
name: string
type: string
age?: number
homeland?: string
}
TypeScriptではユーティリティ型と呼ばれる既存の型から別の型を生み出す型が存在します。型から型を生み出すということは型を変換するともいえ、また型を導き出す関数とみなすこともできます。
TypeScriptのビルトインパッケージで定義されていてimport
なしに利用できるユーティリティ型として、全てのプロパティを必須にするRequired<T>
があります。全てのプロパティを任意にするのはPartial<T>
です。
<T>
のT
の部分には既存の型が入ります。つまり、
type PlayerAllPropertyRequired = Required<Player>
type PlayerAllPropertyOptional = Partial<Player>
と書けば新しく定義されたPlayerAllPropertyRequired
のage
とhomeland
は?
が取れて必須となり、PlayerAllPropertyOptional
のname
とtype
は?
がついて任意となります。
ここまでは簡単です。問題はage
だけ必須にしたり、name
だけ任意にしたりする方法が意外と難しいことです。
解決
あるインターフェースの一部のプロパティを必須にするには、次のようなユーティリティ型を書くといいでしょう。
type RequiredProperties<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
使い方は、
type PlayerSomePropertyRequired = RequiredProperties<Player, "name" | "type" | "age">
となります。この例の場合、name
とtype
は元々必須なので書いても書かなくても同じ結果となります。
解説
どういう仕組みなのか解説します。
まず、K
の後についているextends keyof T
という部分がわかりにくいですが、これはK
が満たすべき制約を示しています。
ユーティリティ型を型の関数だとみなすとK
が(型)変数でkeyof T
が(型)変数K
の型と言ってもいいです。
keyof T
はT
のプロパティをリテラル型として定義し全て結合してユニオン型としたものです。
例えば、keyof Player
は"name" | "type" | "age" | "homeland"
となります。
これを使ってK
の部分を書き直すと、K extends "name" | "type" | "age" | "homeland"
となります。
extends
は指定した型のサブタイプを要求するという制約で、今回はユニオン型に対してなので"name" | "type"
や"type" | "homeland"
などユニオン型に含まれる要素だけで作ったユニオン型を必要とするという意味になります。
まとめると、型定義のRequiredProperties<T, K extends keyof T>
は、
- 型
T
と型K
から新しい型を作る - ただし、
-
T
は何でもよい -
K
はT
の全てのプロパティ名の中から任意のプロパティ名を組み合わせたユニオン型である必要がある
-
という意味になります。
次に定義内容の部分です。Omit<T, K>
とPick<T, K>
はどちらもRequired<T>
と同じようにビルトイン型で
-
Omit<T, K>
: 型T
のプロパティのうち型K
に含まれるプロパティを削除した新しい型を定義する -
Pick<T, K>
: 型T
のプロパティのうち型K
に含まれるプロパティだけを取り出した新しい型を定義する
といったものです。
これを踏まえてOmit<T, K> & Required<Pick<T, K>>
を&
で前後に分解して考えると、
-
Omit<T, K>
: 型T
のプロパティのうち型K
に含まれるプロパティを削除した型 -
Required<Pick<T, K>>
: 型T
のプロパティのうち型K
に含まれるプロパティだけを取り出した型で全てのプロパティが必須
となります。
最後にまとめると、&
は型のマージなので、型T
から型K
に含まれるプロパティを一旦削除しておいて、削除したプロパティだけでできた型の全てのプロパティを必須とする型とマージするということになります。
例えば、
RequiredProperties<Player, "name" | "type" | "age">
は、Omit<T, K> => Omit<Player, "name" | "type" | "age">
の部分で、
interface OmittedPlayer {
homeland?: string
}
となり、
Pick<T, K> => Pick<Player, "name" | "type" | "age">
の部分で、
interface PickedPlayer {
name: string
type: string
age?: number
}
となり、さらにRequired
がつくことで、
interface RequiredPickedPlayer {
name: string
type: string
age: number
}
のようにage
の?
が取れて、最後に
interface OmittedPlayer {
homeland?: string
}
とマージすることで
interface PlayerSomePropertyRequired {
name: string
type: string
age: number
homeland?: string
}
が得られます。