ユーティリティ型 (Utility types)

TypeScriptで開発するということはJavaScriptに型を付与することです。型を付与する時にあらかじめ用意されている便利な型の表現がいくつかありますのでそちらを紹介します。今回紹介するものは全てではないので、他も興味がある方は公式やソースコードを参照してください。

これから紹介するユーティリティ型は全てTypeScriptのパッケージで定義されており、ソースコードも同梱されているのでその実装方法を見ることが可能です。

本章のユーティリティ型の仕様例で、ことわりなくPerson, Userというオブジェクトを使用している部分は、下記に示すタイプエイリアスとそのオブジェクトを使うものとします。

type Person = {
surname: string;
middleName?: string;
givenName: string;
};
type User = {
surname: string;
middleName?: string;
givenName: string;
age: number;
address?: string;
nationality: string;
createdAt: string;
updatedAt: string;
};

Required<T>

全てのプロパティからOptionalであることを意味する?を取り除きます。

Required<Person>は以下と同じ型になります。

type RequiredPerson = {
surname: string;
middleName: string;
givenName: string;
};

Readonly<T>

オブジェクトのプロパティに対する代入を防ぐreadonlyを全てのプロパティに対して適用します。プロパティがオブジェクトだった場合、それが持つプロパティまではreadonlyにしないことに注意してください。これは普通のreadonlyと同じ挙動です。

Readonly<Person>は以下と同じ型になります。

type ReadonlyPerson = {
readonly surname: string;
readonly middleName?: string;
readonly givenName: string;
};

Partial<T>

全てのプロパティにOptionalであることを意味する?を適用します。

Partial<Person>は以下と同じ型になります。

type PartialPerson = {
surname?: string;
middleName?: string;
givenName?: string;
};

より便利な省略可能な引数

関数の引数をOptional parameters, Default parametersDestructuring assignmentPartial<T>を組み合わせることによって省略可能でありながら見やすい関数を実装できます。これらの用語ついては関数のページにて取りあげておりますのでご参照いただければと思います。

ユーザーの検索をかける関数を作り、その属性に応じて検索ができるとします。

function findUsers(name?: string, nationality?: string, age?: number): Promise<User[]> {
// ...
}

ですが、このfindUsers()のシグネチャだと名前、国籍は問わないが年齢だけXX才のユーザーが欲しい時は引数の順番を維持するために他の引数はundefinedを入力しなければいけません。

findUsers(undefined, undefined, 22);

この例題は引数が3個しかないためそこまで見辛くはないですが、多い引数の関数になると、どこに引数を入れて他をundefinedとするかが面倒です。これをPartial<T>を使って見た目をよくできます。

まず引数は全てオブジェクトで受け渡しされるものとしてそのオブジェクトの型を定義します。さらにプロパティを省略可能にするためにPartial<T>をつけます。

type FindUsersArgs = Partial<{
name: string;
nationality: string;
age: number;
}>;

これを関数findUsers()の引数にします。

function findUsers({ name, nationality, age }: FindUsersArgs): Promise<User[]> {
// ...
}

これだけではまだ呼び出し側は省略ができません。findUsers()を使用する時は仮に何も設定する必要がなくても引数に{}を与えなければいけません。

findUsers({});

引数を省略できるようにするためにDefault parametersを使い省略時に{}が代入されるようにします。

function findUsers({ name, nationality, age }: FindUsersArgs = {}): Promise<User[]> {
// ...
}
findUsers();
findUsers({ age = 22 });

FindUsersArgsの右の= {}がそれにあたります。これによりfindUsers()は引数がなくても呼び出せるようになります。特定の引数だけ値をすることもできます。findUsers({ age = 22 })がその例です。

さらにFindUsersArgs側にもDefault parametersを設定することで初期値することもできます。

function findUsers({ name = 'John Doe', nationality = 'Araska', age = 22 }: FindUsersArgs = {}): Promise<User[]> {
// ...
}

Record<K, T>

Index signaturesと似たような効果を持ちます。Kはオブジェクトのキーを意味し、string, number, symbol型またはそれらのユニオン型を指定できます。Tはオブジェクトのプロパティを意味します。Index signaturesと異なりKsymbol型も適用できることに注意してください。

Index signaturesについてはタイプエイリアスの頁を参照ください。

PersonRecordを使って表現すると以下になりますがRecordはプロパティをOptionalにする機能はないためPersonとは完全に一致せず、上記のRequired<Person>と同じものになります。

type Name = 'surname' | 'middleName' | 'givenName';
type Person = Record<Name, string>;

Pick<T, K>

ある巨大なタイプエイリアスから、必要な部分だけを抽出します。KはタイプエイリアスTのキーの部分集合である必要があります。

以下はPickを使ってUserからPersonを作る一例です。

type Necessary = 'surname' | 'middleName' | 'givenName';
type Person = Pick<User, Necessary>;

キーの部分集合である。について

部分集合と聞くと難しいかもしれませんが、言い換えるとキーに存在しないリテラルタイプを指定できないことを意味しています。上記例はUsermiddleName, givenNameNameは大文字から始まりますが、これを小文字にしたタイプエイリアスを定義するとこれはPickでは使用できません。

type Necessary = 'surname' | 'middlename' | 'givenname';
type Person = Pick<User, Necessary>;
// -> Type '"middlename"' is not assignable to type '"surname" | "middleName" | "givenName" | "age" | "address" | "nationality" | "createdAt" | "updatedAt"'.

大元となる型の定義に追従する

書籍を扱うサービスを作ったとして、書籍を意味するオブジェクトBookが以下のように定義されているとします。

type Book = {
id: number;
title: string;
author: string;
createdAt: Date;
updatedAt: Date;
};

これを参考にしてBookを作成するための入力データとしてBookInputDataを作るとします。これは外部からのリクエストで作成され、id, createdAt, updatedAtはこのサービスで後付けで割り当てられるとすればBookInputDataは以下になります。

type BookInputData = {
title: string;
author: string;
};

ここでauthorプロパティがstringではなくPersonになる必要があったとします。Book, BookInputDataを独立して定義しているとこの変更のために都度、各々のauthorプロパティを変更する必要があります。

type Book = {
id: number;
title: string;
author: Person;
createdAt: Date;
updatedAt: Date;
};
type BookInputData = {
title: string;
author: Person;
};

これらの定義が近くにある状態ならまだしも、異なるファイルにあれば非常に探し辛くなります。

そこでBookInputDataPick<T, K>を使って定義しなおします。

type BookInputData = Pick<Book, 'title' | 'author'>;

このようにすればBookInputDataは少なくともBookとコード上の繋がりができる上に、authorプロパティの型変更を自動で追従してくれるようになります。

Omit<T, K>

Pickと逆の動作です。つまり不必要な部分を取り除きます。KはタイプエイリアスTのキーの部分集合である必要はありません。タイポなどに対する検査がPickと比べて貧弱なので注意してください。

以下はOmitを使ってUserからPersonを作る一例です。

type Unnecessary = 'age' | 'address' | 'nationality' | 'createdAt' | 'updatedAt';
type Person = Omit<User, Unnecessary>;

キーの部分集合である必要がない。について

Pickと逆です。UsercreatedAt, updatedAtAtは大文字から始まりますが、これに気づかずに小文字で書いてしまってもこのことに対する指摘はなくOmitの結果はcreatedAt, updatedAtを含んでしまいます。

type Unnecessary = 'age' | 'address' | 'nationality' | 'createdat' | 'updatedat';
type Person = Omit<User, Unnecessary>;
// ->
// {
// surname: string,
// middleName?: string,
// givenName: string,
// createdAt: string,
// updatedAt: string
// }

Exclude<T, U>

ユニオン型TからUを取り除きます。

type Grade = 'A' | 'B' | 'C' | 'D' | 'E';
type PassingGrade = Exclude<Grade, 'E'>;

この例は成績についてです。落第を示す成績が'E'でそれ以外は及第だとすればこのようにして及第を示すタイプエイリアスPassingGradeを作ることができます。

Excludeの注意点

UTの部分集合である制限がありません。つまりOmitと同様、タイポなどに気をつけなければいけません。

以下はPull Requestに関するタイプエイリアスと解釈してください。

type PullRequestState = 'drft' | 'reviewed' | 'rejected';
type MergeableState = Exclude<PullRequestState, 'drft' | 'rejected'>;

MergeableState'reviewed'を意味しますが安易にExcludeを使うと2点の問題があります。

PullRequestStateに新しい状態が追加された時

PullRequestState'testFailed'というMergeableStateに含めたくない状態を追加したとします。するとこの修正に伴ってMergeableStateU(第2ジェネリクス)も同時に修正しないといけません。これを忘れると'testFailed'MergeableStateに含まれてしまいます。

type PullRequestState = 'drft' | 'reviewed' | 'rejected' | 'testFailed';
type MergeableState = Exclude<PullRequestState, 'drft' | 'rejected'>;
// -> 'reviewed' | 'testFailed'

タイポ

PullRequestState'drft'は誤字だったので'draft'に修正しました。これも同様にMergeableStateの第2ジェネリクスの修正を忘れるとMergeableState'draft'が含まれてしまいます。

type PullRequestState = 'draft' | 'reviewed' | 'rejected';
type MergeableState = Exclude<PullRequestState, 'drft' | 'rejected'>;
// -> 'draft' | 'reviewed'

ユニオン型ではない型を指定する

またExcludeの第1ジェネリクスにユニオン型ではない型を指定しても意味がありません。

type InputText = Exclude<string, ''>;
// -> string

入力した文字を表す型としてInputTextを定義したとしても、このInputText''以外の全てのstringとは解釈されず、ただのstring型になります。

Extract<T, U>

Excludeと逆です。ユニオン型TからUを抽出します。

type Grade = 'A' | 'B' | 'C' | 'D' | 'E';
type FailingGrade = Extract<Grade, 'E'>;

落第を表す成績が'E'ならこのようにして落第を表すタイプエイリアスFailingGradeを作ることができます。

Extractの注意点

Ecludeと同様UについてはTの部分集合である制限がありません。タイポに気をつける必要があります。

タイポ

type PullRequestState = 'draft' | 'reviewed' | 'rejected';
type MergeableState = Extract<PullRequestState, 'reviewd'>;

MergeableStateの第2ジェネリクスはタイポです。これに気づかないとMergeableStateneverという型になり、いかなる値も代入できません。

ユニオン型ではない型を指定する

Extractの第1ジェネリクスにユニオン型ではない型を指定するとneverになります。

type Zero = Extract<number, 0>;
// -> never

0を表す型としてZeroを定義したとしても、これはただのnever型になります。