TypeScript で引数の値によって返り値の型を絞り込みたい!!!
INDEX
やりたいこと
- 引数に “A” | “B” の Union 型を取る
- 引数の値が A の時は返り値の型は { readonly type: “A”, readonly foo: striing } として推論される
- 引数の値が A の時は返り値の型は { readonly type: “B”, readonly bar: striing } として推論される
以上の条件を満たす関数 fn を作成したい
:man_gesturing_no: 初めに試した形
function fn(type: "A" | "B") {
switch (type) {
case "A":
return { type: "A", foo: "value" } as const;
case "B":
return { type: "B", bar: "value" } as const;
default: {
const _never: never = type;
throw new Error(_never);
}
}
}
const result = fn("A");
// ^?
// const result: {
// readonly type: "A";
// readonly foo: "value";
// readonly bar?: undefined;
// } | {
// readonly type: "B";
// readonly foo: "value";
// readonly bar?: undefined;
// };
// Why Japanese peopooooooooooooooooole!!!!!!!!!!!!!
まずは愚直に switch 文で引数を振り分けて TypeScript に型推論した結果です。
返り値の型が Union 型になってしまいます。
:man_gesturing_no: 返り値の型を as キャストで明示的に指定
type TypeA = { readonly type: "A"; readonly foo: string };
type TypeB = { readonly type: "B"; readonly bar: string };
function fn(type: "A" | "B") {
switch (type) {
case "A":
return { type: "A", foo: "value" } as TypeA;
case "B":
return { type: "B", bar: "value" } as TypeB;
default: {
const _never: never = type;
throw new Error(_never);
}
}
}
const result = fn("A");
// ^? const result: TypeA | TypeB
// Why Japanese peopooooooooooooooooole!!!!!!!!!!!!!
as キャストで明示的に型指定しているのでオプショナルなプロパティは返り値の型からなくなりましたが、こちらも返り値の型が Union 型のままです。
:man_gesturing_no: 返り値の型に Generics を使う
type TypeA = { readonly type: "A"; readonly foo: string };
type TypeB = { readonly type: "B"; readonly bar: string };
type Result<T> = T extends "A" ? TypeA : T extends "B" ? TypeB : never;
function fn<T extends "A" | "B">(type: T): Result<T> {
switch (type) {
case "A":
return { type: "A", foo: "value" } as const satisfies Result<T>;
~~~~~~ ~~~~~~~~~
case "B":
return { type: "B", bar: "value" } as const satisfies Result<T>;
~~~~~~ ~~~~~~~~~
default: {
const _never: never = type;
throw new Error(_never)
}
}
}
const result = fn("A")
// ^? const result: TypeA
ここでやっと返り値の型の絞り込みができるようになりましたが、代償として TypeScript のエラーが発生するようになりました。
TypeScript のエラー
Type '{ readonly type: "A"; readonly foo: "value"; }' is not assignable to type 'Result<T>'.
Type '{ readonly type: "A"; readonly foo: "value"; }' does not satisfy the expected type 'Result<T>'.
ここで引数の値 type の型を確認してみると、switch 文で絞り込みを行っているにも関わらず、 case “A”: の後の type の型が依然として “A” | “B” の Union 型となっていることがわかります。
:man_gesturing_ok: 返り値の型に as キャストした Generics を使う
type TypeA = { readonly type: "A"; readonly foo: string };
type TypeB = { readonly type: "B"; readonly bar: string };
type Result<T> = T extends "A" ? TypeA : T extends "B" ? TypeB : never;
function fn<T extends "A" | "B">(type: T) {
switch (type) {
case "A":
return { type: "A", foo: "value" } as const satisfies TypeA as Result<T>;
case "B":
return { type: "B", bar: "value" } as const satisfies TypeB as Result<T>;
default: {
const _never: never = type;
throw new Error(_never);
}
}
}
const a = fn("A");
// ^?
const b = fn("B");
// ^?
fn("C"); // Argument of type '"C"' is not assignable to parameter of type '"A" | "B"'.(2345)
as キャストを行っているので完全な型安全とは言えませんが、引数の型によって返り値の型を絞り込むことができるようになりました。
なぜ返り値の型を絞り込むことができないのか
「返り値の型に Generics を使う」で少し触れましたが、引数 type を switch 文で絞り込んでいるにも関わらず、TypeScript は switch 文での絞り込み後の型が以前として “A” | “B” の Union 型となっています。
TypeScript では Generics の型を絞り込むことはできないのか?という予測の元、我々は GitHub の奥地へと向かった。
- Type of a generic is not narrowed · Issue #49676 · microsoft/TypeScript
- Suggestion for Dependent-Type-Like Functions: Conservative Narrowing of Generic Indexed Access Result Type · Issue #33014 · microsoft/TypeScript
参考の issue を見る限りはまず、TypeScript で型パラメータを絞り込むことができないようです。
そのため Generics の型パラメータ T が Union 型のままとなって、TypeScript の型エラーが発生するのだと思われます。
ワークアラウンド(回避策)は先に例示した as キャストによる型アサーションやその他にもオーバーロードで回避は可能なようですが、 前述の通りやはり完全な型安全ではなく、コンパイラを騙くらかすといった回避方法になるようです。
さいごに
完全な型安全とは言えないので、開発者が用法容量を守って正しく使わなければいけませんが、 引数によって返り値の型を絞り込みたいといったユースケースはそこそこ存在するのではないでしょうか。
それにしても TypeScript 難しい。。
この記事を書いた人
- 2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー