Typescript(条件型)

はじめに

今回の記事は型の三項演算子である条件型(Conditional Types)についての記事になります。因みに、Conditionalとは条件次第の、条件を含む、条件付きのと解釈されます。

条件型の基礎

はじめに述べたとおり条件型とはまさに型の三項演算子です。(大切なことなので二度言いました...)

type MyCondition<T, U, X, Y> = T extends U ? X : Y;

ここでは4つの型が存在し、MyConditionは「TがUのサブクラス(部分型)ならばXを、そうでなければY」という型を表現します。
利用方法は以下のとおりです。

type MyCondition<T, U, X, Y> = T extends U ? X : Y;

type A = MyCondition<string, string, true, false>; //true
type B = MyCondition<number, string, true, false>; //false

ただ実際にはこのようなジェネリック型パラメータを宣言するための山括弧<>の数が4つの型(T,U,X,Y)である抽象的な型定義を使用するケースはないように思えます。
以下のような使い方でしょうか。

type IsString<T> = T extends string ? true : false;
//Uはstring、Xはtrue、Yはfalse
type A = IsString<string>; //true
type B = IsString<number>; //false


また条件型をベースとした分配条件型というものが存在します。

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

①type A = IsString<"a" | "A">; //true
②type B = IsString<1 | 2>; //false
③type C = IsString<"a" | 1>; //boolean

①~③に共通していることは山括弧の中身がユニオン型で表されています。
その上でそれぞれを解説すると、
①では'a','A'ともにstring型のサブクラスであるためtrueが返ります。
②は1,2はどちらもstring型のサブクラスでないためfalseが返ります。
③は'a'がstring型のサブクラスですが(この時点でtrueが返る)、1がstrung型のサブクラスでない(ここでfalseが返る)ため最終的にはboolean(true|false)が返ります。

条件型の応用

inferキーワード

inferキーワードとは条件の一部としてジェネリクス型を宣言できる機能です。因みに、inferとは本来Type inference in conditional types の略で条件型における型推論と解釈できます。

一つ例を提示します。

class American {
  constructor(
    public name: string,
    public age: number,
    public PostalCode: number
  ) {}
}
class Japanese {
  constructor(
    public name: string,
    public age: number,
    public PostalCode: string
  ) {}
}

アメリカ在住、日本在住という感じで2つの簡単なクラスを作成しました。2つのクラスのうち、PostalCode(郵便番号)プロパティのみ型が異なります。この後、アメリカ在住、日本在住の人の条件にあったコードを組みたときなどにPostalCodeの型によって違いを出せそうです。
そのような時にinferが使えそうです。

type PostalCode<T> = T extends { PostalCode: infer U } ? U : never;
type EnPostalCode = PostalCode<American>; //型はnumber
type JaPostalCode = PostalCode<Japanese>; //型はstring

PostalCodeという型エイリアスにはジェネリクス型を用いています。条件型にはジェネリクス型をインラインで宣言する(ここではそれがUにあたる)ための独自の構文があります。それがinferキーワードです。PostalCodeではT型にクラス名が入りそのクラスから推測してPostalCodeの型U(numberかstring)を決定すると表現されています。

郵便番号がない国ウガンダ在住のクラスに対しては、決して戻ることのない型neverとなります。

class Ugandan {
  constructor(public name: string, public age: number) {}
}
type PostalCode<T> = T extends { PostalCode: infer U } ? U : never;
type UgPostalCode = PostalCode<Ugandan>; //型はnever