ウェブサイト検索

TypeScript - 条件付き戻り値型を持つジェネリック関数の実装


私は非常に基本的な(作為的な)条件型関数を試しており、予期しないエラーが発生しています:

function test<
  T
>(
  maybeNumber: T
): T extends number ? number : string {
  if (typeof maybeNumber === 'number') {
    return maybeNumber // Type 'T & number' is not assignable to type 'T extends number ? number : string'.
  }

  return 'Not a number' // Type '"Not a number"' is not assignable to type 'T extends number ? number : string'.
}

これは条件付き型のかなり直接的な使用だと思ったので、何が起こっているのかよくわかりません。何かアイデアはありますか?

念のために言うと、私は本当にこの特定の関数を実装しようとしているわけではありません。単に条件付き型を試してみていて、なぜこれが実際にはうまくいかないのかをもっと理解したいのです。

回答 #1

根本的な問題は、TypeScript のコンパイラが制御フロー解析を通じてジェネリック型変数の型を絞り込まないことです。(typeof maybeNumber === "number") をチェックすると、コンパイラは 値 の maybeNumber を number に絞り込むことができますが、型パラメータ の T を number に絞り込むことはできません。そのため、number の値を戻り値の型 T extends number ? number : string に代入しても安全であることを確認できないのです。 コンパイラは現在行っていないいくつかの解析を実行する必要があります。例えば「もし typeof maybeNumber === "number" であり、かつ maybeNumber の型から T を推論した場合、このブロック内では T を number に限定できるので、したがって number extends number ? number : string 型の値を返すべきであり、つまり number です」。しかし、これは実際には起こりません。

これは条件付き戻り型を持つジェネリック関数に関するかなりの問題点です。これについての公式のオープンなGitHub問題はおそらく microsoft/TypeScript#33912 ですが、他にもこの問題が主な問題であるGitHubの問題がたくさんあります。

それが「なぜこれがうまくいかないのか」の答えですか?


もしこれを動作させるためのリファクタリングに興味がない場合は、残りを無視しても構いませんが、言語が変わるのを待つのではなく、この状況で何をすべきかを知ることは依然として参考になるかもしれません。

ここでコールシグネチャを維持する最も単純な回避策は、関数を単一のシグネチャオーバーロードにして、実装シグネチャがジェネリックでないようにすることです。これは本質的に、実装内部の型安全性保証を緩めることになります。

type MyConditional<T> = T extends number ? number : string;
type Unknown = string | number | boolean | {} | null | undefined;

function test<T>(maybeNumber: T): MyConditional<T>;
function test(maybeNumber: Unknown): MyConditional<Unknown> {
  if (typeof maybeNumber === 'number') {
    const ret: MyConditional<typeof maybeNumber> = maybeNumber;
    return ret;
  }
  const ret: MyConditional<typeof maybeNumber> = "Not a number";
  return ret;
}

ここでは、型の安全性を保証しようとできる限りのことを行いました。具体的には、一時的な ret 変数を MyConditional<typeof maybeNumber> と注釈し、maybeNumber の制御フロー解析で絞り込まれた型を使用しています。これにより、チェックを入れ替えた場合(=== を !== に変えて確認するなど)には少なくとも警告が出ます。しかし、通常はもっとシンプルに次のように書いて、後の結果に任せることが多いです:

function test2<T>(maybeNumber: T): MyConditional<T>;
function test2(maybeNumber: any): string | number {
  if (typeof maybeNumber === 'number') {
    return maybeNumber;
  }
  return "Not a number";
}

コードへのプレイグラウンドリンク

回答 #2

正しい答えと現時点での回避策(執筆時点):

type MaybeNumberType<T extends number | string> = T extends number
  ? number
  : string;

function test<T extends number | string>(
  maybeNumber: T,
): MaybeNumberType<T> {
  if (typeof maybeNumber === 'number') {
    return <MaybeNumberType<T>>(<unknown>maybeNumber);
  }

  return <MaybeNumberType<T>>(<unknown>'Not a number');
}

test(3); // 3
test('s'); // Not a number

関連記事