はじめに
TypeScript5.3のBeta版の発表がありました。
そこでは様々な新機能の追加がありましたが、この記事ではswitch(true)
における型の絞り込みについて紹介します。
型の絞り込み
TypeScriptでは制御フローに応じた型の絞り込みを行えます。string | number | undefined
を型として持つような変数が絞り込みが可能な制御フローのスコープ内ではstring | number | undefined
から特定のstring
、number
、undefined
を型として持つ変数として扱われます。
function narrowing(x: string | number | undefined) {
if (typeof x === 'string') {
// xの型はstringとして扱われる
return x;
}
if (x !== undefined) {
// xの型はnumberとして扱われる
return x;
}
// xの型はundefinedとして扱われる
return x;
}
switchによる制御フロー
TypeScript5.2以前ではswitch
文による型の絞り込みは行えません。
function narrowing(x: string | number | undefined) {
switch (true) {
case (typeof x === 'string'):
// xの型はstring | number | undefined
x
break
case (x !== undefined):
// xの型はstring | number | undefined
x
break
default:
// xの型はstring | number | undefined
x
}
}
TypeScript5.3以降ではswitch
文によって以下のように変数の方を絞り込むことが可能です(Playground)。
function narrowing(x: string | number | undefined) {
switch (true) {
case (typeof x === 'string'):
// xの型はstring
x
break
case (x !== undefined):
// xの型はundefined
x
break
default:
// xの型はnumber
x
}
}
ただ、switch
文で内で文脈を読んで絞り込みが行われるようになったわけではないです。以下のような場合は絞り込みは行われないことに注意してください。
function narrowing(x: string | number | undefined) {
switch (typeof x) {
case ('string'):
// xの型はstring | number | undefined
x
break
case ('number'):
// xの型はstring | number | undefined
x
break
default:
// xの型はstring | number | undefined
x
}
}
function narrowing(x: string | number | undefined) {
switch (false) {
case (typeof x === 'string'):
// xの型はstring | number | undefined
x
break
case (typeof x === 'number'):
// xの型はstring | number | undefined
x
break
default:
// xの型はstring | number | undefined
x
}
}
つまり、switch (true)
だった時にcase
の式に合わせてスコープ内で絞り込みが行われるということです。
なぜそうなっているか、どのように実装されているかはこちらのPRを参照してください。
おわりに
この記事ではTypeScript5.3の新機能として追加されるswitch(true)
における型の絞り込みについて紹介しました。
以下のようにオブジェクトの一部によってユニオンを絞り込んで利用したいケースではif文の羅列よりもswitchで一連の分岐を表記すると見やすくなることもあるので選択肢の1つとして利用できるようになると良いと思いました。
type Result<T> = {
success: true;
contents: T
} | {
success: false;
contents: Error
}
function unwrap<T>(x: Result<T>) {
switch (true) {
case x.success:
return x.contents;
default:
throw x.contents;
}
}
type Loadable<T> = {
state: "hasValue";
contents: T;
} | {
state: "loading";
contents: Promise<T>;
} | {
state: "hasError";
contents: unknown;
};
async function getContents<T>(loadable: Loadable<T>) {
switch (true) {
case (loadable.state === 'hasValue'):
return loadable.contents;
case (loadable.state === 'loading'):
return await loadable.contents;
case (loadable.state === 'hasError'):
throw loadable.contents;
}
}