開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. 【TypeScript】タグフィールドが「深い」ところにあるUnion型を絞り込む
【TypeScript】タグフィールドが「深い」ところにあるUnion型を絞り込む

【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フィールドを用いてMemberTypeNormalMemberTypePremiumを判別できます。
ドキュメントでは、このように型を判別することを 型の絞り込み と呼んでいるようです。本稿もこれにならいます。

また、この類の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)
    }
}

スクリーンショット 2020-10-19 11.44.46.png

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
}

まとめ

  • asany型を使わずにすむ方法はいろいろある。TypeScriptを信じろ
TOPに戻る