TypeScript の Conditional Types で複数条件をマージする

1 min read読了の目安(約1700字

TypeScript の Conditional Types 便利ですね。

この記事を読んでいる方はご存知のことと思いますが一応説明を入れておくと、Conditional Types は型定義における条件分岐です。
型 T をとって、それが文字列型と互換性があるかどうかを返す IsString は以下のように書くことができます。

type IsString<T> = T extends string ? true : false

type A = IsString<'a'>     // type A = true
type B = IsString<1>       // type B = false
type C = IsString<boolean> // type C = false

三項演算子のような見た目で、ネストもできます。
型 T, U をとって、それらすべてが文字列型と互換性があるかどうかを返す IsAllString は以下のように書くことができます。

type IsAllString<T, U> = T extends string
  ? U extends string
    ? true
    : false
  : false

type A = IsAllString<'a', 'b'>  // type A = true
type B = IsAllString<'a', 2>    // type B = false
type C = IsAllString<true, 'b'> // type C = false
type D = IsAllString<true, 2>   // type D = false

IsAllString は実際には T と U の両方が文字列型と互換性があるかどうかを一度にチェックしたいはずですが、この例では 2 回条件分岐を使っています。

そこで Tuple を使って以下のように書き換えます。

type IsAllString<T, U> = [T, U] extends [string, string] ? true : false

type A = IsAllString<'a', 'b'>  // type A = true
type B = IsAllString<'a', 2>    // type B = false
type C = IsAllString<true, 'b'> // type C = false
type D = IsAllString<true, 2>   // type D = false

ミソはT extends stringU extends string[T, U] extends [string, string]にまとめたところです。
これで複数の条件分岐を AND 条件としてまとめることができました。

注意点として、以下のような場合には使えません。

type IsEmptyStringArray<T> = T extends string[]
  ? T['length'] extends 0
    ? true
    : false
  : false

type A = IsEmptyStringArray<[]>         // type A = true
type B = IsEmptyStringArray<['a', 'b']> // type B = false
type C = IsEmptyStringArray<1>          // type C = false

文字列の配列型との互換性をチェックしたあとに、その length で条件分岐したいときなどは、前の条件分岐の結果が後の条件分岐に影響を与えるため、使えないです 🥺