TypeScript 5.8で条件付き戻り値型に対するナローイングができるようになりそう(特定の制約を満たす場合)

数日前にTypeScript 5.7 RCがアナウンスされてリリースが楽しみだなー!ってところなんだけど、そのさらに数日前に、ウォッチしていたこのPR↓がマージされてTypeScript 5.8.0のマイルストーンに入った。わー!

これが今日のお話。TypeScript 5.8.0でConditional return type narrowingが入りそう。楽しみ!

Conditional return type narrowing?

直訳すると「条件付き戻り値型の絞り込み」かな。引数の型によって戻り値の型が変わる関数を定義したいときに、例えばこんな風に書きたくなる。

declare const record: Record<string, string[]>;
declare const array: string[];

function getObject<T extends string | undefined>(
  group: T
): T extends string ? string[] : T extends undefined ? Record<string, string[]> : never {
  if (group === undefined) {
    return record;
  }
  return array;
}

const arrayResult = getObject("group");
const recordResult = getObject(undefined);

この例では、引数である group の型が文字列(またはそのサブタイプ)だったら文字列の配列を、undefined だったら Record<string, string[]> を返すように定義してある。

でも、このコードはエラーになる

2024-11-07現在のTypeScript 5.6.3では、このコードはエラーになる。呼び出す側のコードはエラーにならずに、渡した引数の型に合わせて正しく戻り値の型が判断されるんだけど(arrayResultの型はstring[]になっている)、関数の実装側の return 部分がエラーになる。

if ブロックの中に入るってことは groupundefined だってことだから戻り値の型は Record<string, string[]> だし、if ブロックの中に入らなかったら文字列ってことなんだから戻り値の型は string[] やん!」って思うけどTypeScriptは分かってくれない。

そこは分かってくれるとうれしいなぁ。という要望に対応したのが、今回のPR。

分かってくれる。そう、5.8ならね。

やったー!↑は、5.8.0-dev.20241106を使っている(画像だけ貼られてもなぁというのはそれはそう)。

制約

この特別なチェックは、一般的な条件付き戻り値型に対して実施できるわけではなくて、制約がある。その制約とは↓この形式になっていること。

T extends A ? AType : T extends B ? BType : never

つまり

それに加えて

  • 上記の ABT の構成要素になっていること

ということで全体としてはこんな形↓になる。

function fun<T extends A | B>(param: T): T extends A ? AType : T extends B ? BType : never {
    if (isA(param)) { ... }
    else { ... }
}

この形なら、戻り値の条件型に対するナローイングが効く。

インデックスアクセス型

インデックスアクセス型の場合も上記の条件を満たすので、ナローイングができる。

9月に書いたこの記事で

↓これがエラーになると書いたが、5.8ではエラーにならない。やったねー。

type Mapping = {
    a: boolean,
    b: string,
}

function getValue<K extends "a" | "b">(key: K): Mapping[K] {
    if (key === "a") {
        return true;
    } 
    return "foo";
}

おしまい

面白いなー。制約はあるけど「まさにこういうことがやりたい」ってときにシュッと思い出せたら便利だなー!