【TypeScript】タグフィールドが「深い」ところにあるUnion型を絞り込む
TL;DR
- 2階層目以降の「深い」フィールドを用いてUnion型を絞り込むことはできない
- ユーザ定義の型ガードを使うことできれいに解決できる
- 本稿のコード: Playground
言葉の定義、Union型の絞り込み
TypeScript公式ドキュメント/Union型の判別(Discriminated Union)
literal型のメンバを持つクラスがある場合、そのプロパティを使用して、Union型のメンバを判別することができます。
type MemberTypeNormal = {
type: "normal" // タグ
name: "通常会員"
}
type MemberTypePremium = {
type: "premium" // タグ
name: "プレミアム会員"
}
type MemberType = MemberTypeNormal|MemberTypePremium
function exhaustiveCheck(_: never): never
{
throw new Error("到達しないはず")
}
function handleMemberType(memberType: MemberType): void {
switch(memberType.type) {
case "normal":
memberType // MemberTypeNormal
return
case "premium":
memberType // MemberTypePremium
return
default:
exhaustiveCheck(memberType) // never
}
}
MemberType
型のトップレベルのtype
フィールドを用いてMemberTypeNormal
とMemberTypePremium
を判別できます。
ドキュメントでは、このように型を判別することを 型の絞り込み と呼んでいるようです。本稿もこれにならいます。
また、この類のUnion型は Tagged Union と呼ばれることがあり、type
フィールドは タグ などと呼ばれます。本稿ではこれにならいます。
2階層目以降にタグがある場合
type MemberTypeNormal = {
type: "normal" // タグ
name: "通常会員"
}
type MemberTypePremium = {
type: "premium" // タグ
name: "プレミアム会員"
}
type MemberNormal = {
memberType: MemberTypeNormal
}
type MemberPremium = {
memberType: MemberTypePremium
paymentMethod: string
}
type Member = MemberNormal|MemberPremium
function exhaustiveCheck(_: never): never
{
throw new Error("到達しないはず")
}
function handleMember(member: Member): void {
switch(member.memberType.type) { // 2階層目
case "normal":
member // Member
return
case "premium":
member // Member
// TS2339: Property 'paymentMethod' does not exist on type 'Member'.
// Property 'paymentMethod' does not exist on type 'MemberNormal'.
member.paymentMethod
const memberPremium = member as MemberPremium // これは絶対やりたくない
memberPremium.paymentMethod
return
default:
// TS2345: Argument of type 'Member' is not assignable to parameter of type 'never'.
// Type 'MemberNormal' is not assignable to type 'never'.
exhaustiveCheck(member)
}
}
TypeScriptバージョン4.0.2時点で、member.memberType.type
のような「深い」フィールドを用いてUnion型を絞り込むことはできません。
しかしas
などは使いたくない...
ユーザ定義のType Guard
ユーザ定義のType Guard(User-Defined Type Guard)
を使うことできれいに解決できます。
function isMemberNormal(member: Member): member is MemberNormal
{
return member.memberType.type === "normal";
}
function isMemberPremium(member: Member): member is MemberPremium
{
return member.memberType.type === "premium"
}
boolean
型を返すような関数で、戻り値型を 仮引数 is Type
とすることで、以降の処理ではその実引数がType
型であることをコンパイラに伝えることができます。
type MemberTypeNormal = {
type: "normal"
name: "通常会員"
}
type MemberTypePremium = {
type: "premium"
name: "プレミアム会員"
}
type MemberNormal = {
memberType: MemberTypeNormal
}
type MemberPremium = {
memberType: MemberTypePremium
paymentMethod: string
}
type Member = MemberNormal|MemberPremium
function exhaustiveCheck(_: never): never
{
throw new Error("到達しないはず")
}
// User-Defined Type Guards
function isMemberNormal(member: Member): member is MemberNormal
{
return member.memberType.type === "normal";
}
function isMemberPremium(member: Member): member is MemberPremium
{
return member.memberType.type === "premium"
}
function handleMember2(member: Member): void {
if(isMemberNormal(member)) {
member // MemberNormal
return
}
if (isMemberPremium(member)) {
member // MemberPremium
member.paymentMethod // ok
return
}
exhaustiveCheck(member) // neverに絞り込まれる。ok
}
まとめ
as
やany
型を使わずにすむ方法はいろいろある。TypeScriptを信じろ