yuu_1st
@yuu_1st

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

引数のbooleanで戻り値の型を変化させたい

解決したいこと

typescriptで、引数にbooleanを渡すことによって、その値によって戻り値の型を変化させたい。

例)

function getString<R extends boolean>(require: R): R extends true ? string : string | null{
  if (require === true) {
    return requireString(); // string
  } else {
    return optionalString(); // string | null
  }
}

発生している問題・エラー

return optionalString()にて以下のエラーが発生します。

Type 'string | null' is not assignable to type 'R extends true ? string : string | null'.
  Type 'null' is not assignable to type 'R extends true ? string : string | null'.

getString<boolean>(false);と呼び出すことで、戻り値の型がstringになっているにも関わらず、実際はstring | nullが返ってくるという状態が発生するので、エラー自体は起こりうる状態の提示をされていると思います。
ただ、trueとfalseを全て入れ替えた以下のコードでも同様のエラーが発生します。

function getString<R extends boolean>(require: R): R extends false ? string | null : string{
  if (require === false) {
    return optionalString(); // 同じエラーが発生する
  } else {
    return requireString();
  }
}

こちらでもエラーが発生する現実的なRの値は思いついていないのですが、if (require === false)内でも、requireの型がR extends booleanになっているため、そこからエラーになっているようです。

ここからが本題なのですが、上記の通り、引数requireによって戻り値の型を変化させたいのですが、この時、上記エラーが出ないように適切に分岐させたコードを記述をする方法はあるのでしょうか?
return optionalString() as R extends false ? string | null : string;
と、asを用いた記述をするとエラー自体は消えるのですが、それ以外でもし他の方法があれば教えていただきたいです。

0

2Answer

選択肢としては、関数オーバーロードを使って

function getString(require: true): string
function getString(require: false): string | null
function getString(require: boolean): string | null{
  if (require === true) {
    return requireString(); // string
  } else {
    return optionalString(); // string | null
  }
}

と書く方法があります


元の記法で何が怒られているかを補足しておくと、

(質問文)

function getString<R extends boolean>(require: R): R extends true ? string : string | null{
  if (require === true) {
    return requireString(); // string
  } else {
    return optionalString(); // string | null
  }
}

と書くことは、あくまで便宜的なものでトランスパイラの実態とは違いますが理解を助けるための擬似的なコードになおすと、

function getString1(require: boolean): string | null{
  if (require === true) {
    return requireString(); // string
  } else {
    return optionalString(); // string | null
  }
}

function getString2(require: true): string {
  if (require === true) {
    return requireString(); // string
  } else {
    return optionalString(); // string | null
  }
}

という2つの関数を定義するのと大体同じことになります

後者の関数をエディタに書いたりトランスパイルしたりしてみると分かると思いますが、返り値をstringとして宣言しているにも関わらずstring | nullを返すコードパスが存在するので、トランスパイルエラーとなります
現実的にはelse文に到達する可能性はないのでreturn値は無視してくれ〜と人間的には思うのですが、それをトランスパイラとして判断するのは難しいのです

3Like

Comments

  1. @yuu_1st

    Questioner

    回答ありがとうございます
    確かに関数オーバーロードでも可能ですね
    ただ今回は呼び出し側でRを明示しなければ戻り値の型が適切に取得できることと、型の安全性がジェネリクスを使った場合と同じく指定間違いを起こせるので、候補の1つ、という感じになりそうです。。

    補足の方も確認したのですが、requireがtrue型だけだとしても、`return optionalString();`はエラーを出力するのですね。知らなかったです。
    else文の域だとrequireはnever型になるので、到達不能ブロックとして認識してくれればいいのですが・・・switch文に書き換えても同様にエラーが出たので、到達する可能性が何かしらあると判断されるのですかね。。

この関数だけを見るとなんでダメなのかわかりにくいですが、この関数を呼び出す側のことを考えると合点がいくと思います。

let t = getString(true);  // string
let f = getString(false); // string | null
let b = getString(flag);  // flagの内容は実行時まで確定しないので、トランスパイルの時点ではどちらの型を返すのか特定できない

なので期待している書き方を実現するには、引数の制約として「定数のみ」を指定できるような仕組みが必要でしょうね。

0Like

Comments

  1. @yuu_1st

    Questioner

    回答ありがとうございます。
    ただ、flagのようなboolean型の変数で渡した時、bの型は
    `typeof flag extends true ? string : string | null`
    で済むので、わざわざ特定する必要はないと思います。(実際に同様のコードを記述すると、bの型は`string | null`に落ち着いているようです。)

    補足になりますが、今回の`戻り値の型を変化させたい`箇所は、この関数の呼び出し元ではなく、コード内の`return`の位置になります(エラーが出ている箇所です)。質問文の意図がずれるような記述になってしまっていたのは申し訳ないです。

Your answer might help someone💌