TypeScriptで複数のユニオン型を定義し、それらのユニオン型に重複がないかをチェックしたい場合があります。例えば、以下のように複数のユニオン型を定義したとします。
type SubUnion1 = "one" | "two";
type SubUnion2 = "two" | "three";
type SubUnion3 = "nine" | "ten";
これらのユニオン型を1つのユニオン型にまとめたいとき
次のように書くとts的にはエラーが出ないので重複に気づくことができません。
/// Initial type:"one" | "two" | "two" | "three" | "nine" | "ten" となり特にエラーは出ない
type Union = SubUnion1 | SubUnion2 | SubUnion3
ユニオン型の重複チェック方法
まず、各ユニオン型をタプルにまとめます。
type UnionList = [SubUnion1,SubUnion2,SubUnion3]
次に、重複があるかどうかをチェックする型を定義します。この型は、重複がなければneverを返し、重複があれば重複情報を含むタプルのユニオン型を返します。
type Overlaps<T extends string[]> = {
[K in keyof T]: {
[L in keyof T]: L extends K
? never
: T[K] & T[L] extends never
? never
: ['次のインデックスに', K | L, '次の重複があります', T[K] & T[L]];
}[number];
}[number];
この型は複雑なのでOverlaps<UnionList>と定義したものとして
一つずつ説明します。
- 外側のマッピング
[K in keyof T]
これは、タプルTの各要素に対してインデックス K を使ってマッピングを行います。
Kはタプルのインデックスを表します。
(KはUnionListのインデックスである0,1,2) - 内側のマッピング
[L in keyof T]
内側のマッピングでは、同じタプルTの各要素に対してインデックスLを使ってマッピングを行います。
(同様にTはUnionListのインデックスである0,1,2)
これにより、二重ループのような構造が作られ、各要素の組み合わせ (KとL) をチェックすることができます。 - インデックスが同じ場合の処理
L extends K ? never:
インデックスLがKと同じ場合(つまり、同じ要素同士の場合)は処理をスキップするためにneverを返します。 - 重複がない場合の処理
T[K] & T[L] extends never ? never
T[K]とT[L]の交差型がneverである場合、つまり重複がない場合はneverを返します。
(SubUnion2とSubUnion3の比較の時、交差型がneverなのでneverを返す) - 重複がある場合の処理
['次のインデックスに', K | L, '次の重複があります', T[K] & T[L]]
T[K]とT[L]の交差型がneverでない場合、重複があるとみなします。
重複がある場合、重複の詳細情報を含むタプルを生成します。ここでは、インデックスKとL、および重複している値T[K]&T[L]を含むタプルを作成します。
(SubUnion1とSubUnion2の比較の時、交差型がtwoなので['次のインデックスに', 0|1, '次の重複があります',twoを返す) - 内側のマッピングの結果をユニオン型に変換
[number]
内側のマッピングの結果をユニオン型に変換します。これにより、全てのインデックスの結果が1つのユニオン型にまとめられます。 - 外側のマッピングの結果をユニオン型に変換
[number]
同じく、外側のマッピングの結果もユニオン型に変換します。
次に、重複がある場合にコンパイルエラーを発生させるための型関数を定義します。
type ExpectNever<T extends never> = void;
これらを組み合わせて、重複チェックを行います。
// エラーが発生する TS2344: Type ["次のインデックスに", "0" | "1", "次の重複があります", "two"] does not satisfy the constraint never
type EnsureNoOverlapsOfSubUnions = ExpectNever<Overlaps<SubUnions>>;
最後に、タプルの要素をユニオン型にまとめることで、元のユニオン型を取得できます。
type Union = UnionList[number];
まとめ
以上をまとめると次のようになります。
type UnionList = [SubUnion1,SubUnion2,SubUnion3]
type Union = UnionList[number]
type Overlaps<T extends string[]> = {
[K in keyof T]: {
[L in keyof T]: L extends K
? never
: T[K] & T[L] extends never
? never
: ['次のインデックスに', K | L, '次の重複があります', T[K] & T[L]];
}[number];
}[number];
type ExpectNever<_ extends never> = void;
// 重複を確認するための型重複があった場合下記がエラーになり気づける
type EnsureNoOverlapsOfSubUnions = ExpectNever<Overlaps<UnionList>>;