🐹

TypeScript で関数の引数の型を推論しつつ、条件を満たさない場合に対象の引数部分をエラーにする方法

2024/03/22に公開

ドワンゴのニコニコ生放送でフロントエンドを担当している misuken です。

今回は TypeScript で関数の引数の型を推論しつつ、条件を満たさない場合に対象の引数部分をエラーにする方法を紹介します。

はじめに

この記事を書くきっかけになったのは、yutake27さんの記事 TypeScriptで知ってコードの安全性が上がったtips集 で紹介されていた 配列がUnion型の全ての要素を網羅しているか確認する を拝見したためでした。

// https://qiita.com/uhyo/items/e9a03aa4ea2db8d1d7fe から引用
type ElementOf<A extends any[]> = A extends (infer Elm)[] ? Elm : unknown;
type IsNever<T> = T[] extends never[] ? true : false;

function allElements<V>(): <Arr extends V[]>(arr: Arr) =>
    IsNever<Exclude<V, ElementOf<Arr>>> extends true ? V[] : {notFound: Exclude<V, ElementOf<Arr>>} {
    return arr => arr as any;
}

// 適当なUnion型定義
type Fruit = "apple" | "banana" | "lemon";

// OK: Union型の要素全てを網羅している場合
const fruits1: Fruit[] = allElements<Fruit>()(["apple", "banana", "lemon"]);

// エラー: Union型の要素全てを網羅していない場合
const fruits2: Fruit[] = allElements<Fruit>()(["apple", "banana"]); // "lemon"が足りない
// Type '{ notFound: "lemon"; }' is missing the following properties from type 'Fruit[]': length, pop, push, concat, and 26 more.

このコードは機能するのですが、uhyoさんの元記事でも触れられている通り、 Fruit[] をジェネリクスと代入時の2回書かねばならないのが面倒になります。
https://qiita.com/uhyo/items/e9a03aa4ea2db8d1d7fe

そこで、代入するときに型を書かずとも、対象の引数の部分をピンポイントでエラーにする方法を紹介します。

引数の部分でエラーにする方法

関数の定義を以下のようにします。

// 一部元の allElements で簡略化できる部分を簡略化しています
function allElements<V>(): <Arr extends V[]>(
  arr: Arr & (Exclude<V, Arr[number]> extends never[] ? {} : { notFound: Exclude<V, Arr[number]> }),
) => V[] {
  return arr => arr as V[];
}

ポイントは引数の型の以下の部分です。

Arr & (Exclude<V, Arr[number]> extends never[] ? {} : { notFound: Exclude<V, Arr[number]> })

引数 arr のジェネリクス型 Arr が推論され、条件式を満たしていれば Arr & {} つまり Arr になります。

条件式満たしていない場合は Arr & { notFound: Exclude<V, Arr[number]> } となり、引数に渡したものと矛盾するのでエラーになります。

一応以下でも同様のことができるのですが、上記のほうが推論部分と検証部分が独立しているのでわかりやすくなります。

(Exclude<V, Arr[number]> extends never[] ? Arr : { notFound: Exclude<V, Arr[number]> })

使用例

代入時に型を書かなくても、不正なものは引数の部分がエラーになるので期待通りに動作します。

// 適当なUnion型定義
type Fruit = "apple" | "banana" | "lemon";

// OK: Union型の要素全てを網羅している場合
const fruits1 = allElements<Fruit>()(["apple", "banana", "lemon"]);

// エラー: Union型の要素全てを網羅していない場合
const fruits2 = allElements<Fruit>()(["apple", "banana"]); // "lemon"が足りない
// Property 'notFound' is missing in type '("apple" | "banana")[]' but required in type '{ notFound: "lemon"; }'.

引数の場所にエラーが出ている様子

Playground

まとめ

TypeScript で関数の引数の型を推論しつつ、条件を満たさない場合に対象の引数部分をエラーには、引数の型に条件分岐型を交差させることで実現でき、エラー発生箇所の特定や理解がしやすいコードになります。

uhyoさんの元記事 にある議論の中でも同様の解決案が話されていました。

理想的には、そうなった時点で型エラーが発生して欲しいです。そのような「型エラーを発生させる型」というのはやはり需要があるらしく議論があります

いずれ TypeScript の標準でこういった部分を解決する機構が入ると便利ですね。

株式会社ドワンゴでは、様々なサービス、コンテンツを一緒につくるメンバーを募集しています。 ドワンゴに興味がある。または応募しようか迷っている方がいれば、気軽に応募してみてください。

Discussion