検索
連載

TypeScriptの型操作にまつわるあれこれTypeScriptのTypeあれこれシリーズ(11)

altJS、すなわち、JavaScriptの代わりとなる言語の筆頭であるTypeScript。そのTypeScriptは、言語名が示す通り、JavaScriptにType、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptのType(型)に関して、さまざまな方向から紹介していきます。前回は、データ型を定義できる仕組みとしてインタフェースについて、あれこれ紹介しました。本連載も、いよいよ最終回です。その最終回として、既存の型定義やオブジェクトから新たな型定義を作成できるユーティリティー型について、あれこれ紹介していきます。

Share
Tweet
LINE
Hatena
「TypeScriptのTypeあれこれシリーズ」のインデックス

連載:TypeScriptのTypeあれこれシリーズ

ユーティリティー型とは

 TypeScriptには、既存の型定義やオブジェクトから新たな型定義を作成できる仕組みがあります。これをユーティリティー型といいます。

全てをオプションにできるPartialユーティリティー

 ユーティリティー型の具体例を、1つ紹介します。例えば、リスト1のようなコードがあるとします。

interface BMIData {  // (1)
	name: string;
	height: number;
	weight: number;
	age: number;
}
function showBMIData(data: BMIData) {  // (2)
	:
}
const taro: BMIData = {  // (3)
	name: "田中太郎",
	height: 170.5,
	weight: 70.4,
	age: 22
}
showBMIData(taro);  // (4)
const jiro = {  // (5)
	name: "鈴木二郎"
}
showBMIData(jiro);  // (6)
リスト1

 リスト1の(1)は、前回のリスト4のインタフェースBMIDataと同じです。(2)は、このBMIDataを引数の型とする関数showBMIData()です。(4)のようにこの関数を利用する場合、引数として渡すオブジェクトtaroは、(3)のようにBMIDataの全てのプロパティが定義されている必要があります。そのため、(5)のようにnameプロパティしか定義されていないjiroオブジェクトを、(6)のように引数としてshowBMIData()を実行しようとすると、エラーとなります。

 ところが、引数をリスト2の(1)のように記述すると、(2)や(3)のようにtaroやjiroを渡してもエラーとなりません。

function showPartialBMIData(data: Partial<BMIData>) {  // (1)
	:
}
showPartialBMIData(taro);  // (2)
showPartialBMIData(jiro);  // (3)
リスト2

 この(1)の引数で記述した「Partial<>」が型変換ユーティリティーの一つであり、ジェネリクス(< >の中)で指定したオブジェクト型のプロパティを全てオプション(省略可能)へと変更します。すなわち、Partial<BMIData>は、リスト3の型定義と同等の型を表すようになります。

{
	name?: string;
	height?: number;
	weight?: number;
	age?: number;
}
リスト3

型変換ユーティリティーの一覧

 このように、既存の型定義やオブジェクトを操作して、新たな型定義を作成できるユーティリティーの主なものを、表1にまとめておきます。次節以降、順に紹介していきます。

変換対象 ユーティリティー 変換内容
プロパティシグネチャ Partial<Type> Typeの全てのプロパティをオプションに変更
Required<Type> Typeの全てのプロパティを必須に変更
Readonly<Type> Typeの全てのプロパティをreadonlyに変更
Pick<Type, Keys> Typeの中からKeysに該当するプロパティを抽出
Omit<Type, Keys> Typeの中からKeysに該当するプロパティを削除
インデックスシグネチャ Record<Keys, Type> キーがKeysで値がTypeの連想配列の作成
ユニオン型 Exclude<UnionType, ExcludedMembers> UnionTypeからExcludedMembersを除外
Extract<Type, Union> TypeからUnionと共通のものを抽出
NonNullable<Type> Typeからnull系のものを除外
コールシグネチャ Parameters<Type> 引数の型を抽出
ReturnType<Type> 戻り値の型を抽出
表1 主な型変換ユーティリティー

ユーティリティー型の結果を基に型定義も可能

 リスト2では、引数の型としてPartialを直接記述しましたが、このPartialの結果を基に新たな型名を定義することも、もちろん可能です。その場合は、リスト4の(1)のようにtypeキーワードを利用します。

type BMIDataPartial = Partial<BMIData>;  // (1)
function showPartialBMIData(data: BMIDataPartial) {  // (2)
	:
}
リスト4

 リスト4の(1)のように、ユーティリティー型としてBMIDataPartialを一度定義しておくと、(2)のようにその名称で型指定できるようになります。

プロパティシグネチャを変更するユーティリティー型

 前節で紹介したPartialに続いて、表1のプロパティシグネチャを変換対象とするものを紹介していきます。

RequiredとReadonly

 RequiredとReadonlyについては、その使い方はPartialとほぼ同じであり、型の変換内容も表1記載の通りです。例えば、リスト5の(1)のようなPersonalDataがあるとします。この型定義に対して、(2)のようにRequiredを適用して作成されたPersonalDataRequiredは、本来省略可能なプロパティであるはずのmailやtelを省略できなくなります。

 また、(3)のようにReadonlyを適用して作成されたPersonalDataReadonlyは、全てのプロパティがreadonlyとなります。

interface PersonalData {  // (1)
	name: string;
	mail?: string;
	tel?: string;
}
type PersonalDataRequired = Required<PersonalData>;  // (2)
type PersonalDataReadonly = Readonly<PersonalData>;  // (3)
リスト5

PickとOmit

 PickとOmitについては、ジェネリクスの型指定が2個になります。具体的には、リスト6のようなコードとなります。

type WeightAndHeight = Pick<BMIData, "weight"|"height">;  // (1)
type NameAndAge = Omit<BMIData, "weight"|"height">;  // (2)
リスト6

 リスト6の(1)や(2)のジェネリクスの1つ目として指定しているBMIDataは、リスト1の(1)と同じものです。PickとOmitは、2つ目のジェネリクスとして、1つ目のジェネリクスとして指定したオブジェクト型(リスト6ならばBMIData)のプロパティ名を文字列として記述します。複数列挙する場合は、リスト6のようにパイプ(|)で区切ります。するとPickの場合は、2つ目のジェネリクスに記述したプロパティのみの型を作成します。逆にOmitは、2つ目のジェネリクスに記述したプロパティを除外した型を作成します。

 そのため、WeightAndHeightはリスト7の(1)と、NameAndAgeは(2)と同じ型定義となります。

interface WeightAndHeight {  // (1)
	height: number;
	weight: number;
}
interface NameAndAge {  // (2)
	name: string;
	age: number;
}
リスト7

連想配列の型を作成するRecord

 前回、連想配列の型定義の仕組みとしてインデックスシグネチャを紹介しました。それと同じような型定義ができるユーティリティーとしてRecordがあります。

Recordの使い方の基本

 前回のリスト10の(1)で定義したNumberMapと同じものを、Recordを利用して定義した場合は、リスト8のようなコードになります。

type NumberMap = Record<string, number>;
リスト8

 Recordのジェネリクスの1つ目にキーの型を、2つ目に値のデータ型を記述します。このようにして定義されたNumberMapは連想配列の型となり、前回のリスト10の(2)以降のコードと同じものが記述できます。

Recordのキーと値のバリエーション

 このRecordの値部分の型指定、すなわち2つ目のジェネリクスの型指定として、リスト9のようにプリミティブ型以外のものも指定できます。

type PersonList = Record<string, BMIData>;
リスト9

 これは、インデックスシグネチャでも同じことが可能で、リスト10のように記述します。

interface PersonList {
	[key: string]: BMIData;
}
リスト10

 一方、インデックスシグネチャではエラーとなってできない型定義として、キーを限定することがRecordには可能です。これは、リスト11のような記述です。

type PersonList = Record<"taro"|"jiro"|"saburo", BMIData>;
リスト11

 このようにして定義されたPersonList型のオブジェクトは、そのプロパティとしてtaro、jiro、saburo以外が定義できないようになります。これ以外のプロパティとして、例えばshiroを定義しようとすると、図1のようにエラーとなります。


図1 指定のプロパティ以外を定義しようとしてエラーとなった画面

 なおリスト11では、キー部分に文字列リテラルをパイプ(|)で区切って直接記述しましたが、第9回で紹介したリテラル型を利用して、リスト12のような記述も可能です。NameListが、リテラル型です。

type NameList = "taro"|"jiro"|"saburo";
type PersonList = Record<NameList, BMIData>;
リスト12

オブジェクトのキーをリテラル型として抽出するkeyof

 リスト12では、NameListとしてリテラル型を直接定義して、それをRecordのキーに指定しています。このリテラル型を、オブジェクトのプロパティ名から抽出する演算子として、keyofがあります。これを利用すると、例えばリスト13のようなコードが可能となります。

interface Persons {  // (1)
	taro: string;
	jiro: string;
	saburo: string;
}
type NameList = keyof Persons;  // (2)
type PersonList = Record<NameList, BMIData>;  // (3)
リスト13

 リスト13では、(1)でtaro、jiro、saburoをそれぞれプロパティとするオブジェクト型のPersonsを定義しています。このPersonsに対して(2)でkeyof演算子を適用しています。すると、そのプロパティ名をリテラル型とする型定義が自動生成されます。結果、(2)のNameListは、リスト12のNameListと同じ内容となります。この方式を採用する場合、(1)の型定義にshiroを追加すると、自動的に(3)のPersonListのキーとしてshiroが追加され、図1のエラーは発生しなくなります。

ユニオン型を変換対象とするユーティリティー

 次に、ユニオン型を変換対象とするユーティリティーを紹介します。

ユニオン型からnull系を除外するNonNullable

 表1記載の順序とは違いますが、ユニオン型の操作として分かりやすいものとして、NonNullableを先に紹介します。具体的には、リスト14のようなコードです。

type Several = BMIData | Persons | string | undefined | null | never | void | number;  // (1)
type NonNullSeveral = NonNullable<Several>;  // (2)
リスト14

 リスト14の(1)では、さまざまなデータ型のユニオン型であるSeveralを定義しています。これはあくまでもサンプルなので、このようなSeveralが実際に利用されるかどうかはここでは不問とします。このSeveralに対して、(2)のようにNonNullableユーティリティーを適用すると、(1)の中からnull系に該当するundefined、null、neverが除外されたユニオン型が作成されます(図2)。


図2 Severalからnull系が除外されたNonNullSeveral型

 このように、ユニオン型を変換対象とするユーティリティーでは、ジェネリクスで指定されたユニオン型から特定のもののみを除外、あるいは抽出したユニオン型を作成できます。

除外と抽出対象を指定できるExcludeとExtract

 前項で紹介したNonNullableユーティリティーは、除外対象としてはnull系と、あらかじめ決まったものでした。これを、ジェネリクスとして指定できるのが、ExcludeとExtractです。これらは、例えばリスト15のようなコードとなります。

type ExcludedSeveral = Exclude<Several, string|undefined|Date>;  // (1)
type ExtractedSeveral = Extract<Several, string|undefined|Date>; // (2)
リスト15

 リスト15の(1)でExcludeを適用したExcludedSeveralを、(2)でExtractを適用したExtractedSeveralを定義しています。そのどちらも、ジェネリクスの1つ目の型指定として記述したSeveralは、リスト14の(1)のユニオン型です。次に、2つ目のジェネリクスとして「string|undefined|Date」というユニオン型を指定しています。すると(1)のExcludeでは、Severalから「string|undefined|Date」に該当するものを除外したユニオン型が定義されます(図3)。なおDateに関しては、もともとSeveralに含まれていませんので、そもそも除外するかどうかの対象外となります。


図3 Severalからstring|undefined|Dateが除外されたExcludedSeveral型

 一方、(2)のExtractでは、Severalから「string|undefined|Date」に該当するものを抽出したユニオン型が定義されます(図4)。その際、Dateが抽出結果であるExtractedSeveralに含まれていない点に注意してください。ジェネリクスの1つ目と2つ目の両方のユニオン型に共通したもののみが、新たなユニオン型として抽出されるようになります。


図4 Severalからstring|undefined|Dateと共通のものが抽出されたExtractedSeveral型

 なお、ExcludeもExtractも、ジェネリクスの2つ目の型指定として、FunctionやObjectといった記述も可能です。その場合は、関数の型定義のみ、オブジェクトの型定義のみを、除外、抽出できます。

コールシグネチャを変換対象とするユーティリティー

 型変換ユーティリティーを紹介する最後の節として、表1の残りの2個であるコールシグネチャを変換対象とするものを紹介します。

関数の型定義から引数や戻り値を抽出するParametersとReturnType

 ParametersとReturnTypeは、関数の型定義から引数の型や戻り値の型を抽出するユーティリティーです。具体的には、リスト16のようなコードとなります。

type Calc2RandFunc = (rand1: number, rand2: number) => number;  // (1)
type Calc2RandFuncParams = Parameters<Calc2RandFunc>;  // (2)
type Calc2RandFuncReturn = ReturnType<Calc2RandFunc>;  // (3)
リスト16

 リスト16の(1)は、第9回のリスト6の(1)と同じコードです。このような関数の型定義に対して、(2)のようにParametersユーティリティーを適用すると、引数のデータ型をタプル型として定義できます(図5)。


図5 関数の引数をタプル型として定義できるParametersユーティリティー

 同様に、戻り値のデータ型を定義できるのが、(3)のReturnTypeユーティリティーです。(1)の戻り値の型定義がnumberなので、(3)のCalc2RandFuncReturnはnumberと同義になります。

型を抽出するtypeof演算子

 JavaScript/TypeScriptには、型判定として利用されるtypeof演算子があります。TypeScriptでは、このtypeof演算子を型定義として利用できます。すると、既存の関数から型定義を抽出することも可能となります。例えば、リスト17のようなコードです。

function loopFunc(value: number, index: number, array: number[]): void {  // (1)
	:
}
type LoopFuncType = typeof loopFunc  // (2)
リスト17

 リスト17の(1)は、第3回のリスト17中の関数式と同じシグネチャを、関数loopFunc()として定義したものです。このような関数に対して、(2)のようにtypeof演算子を実行し、その結果を型定義とすることができます。このようにして定義されたLoopFuncType型は、まさに第3回のリスト19と同じ型となります(図6)。


図6 typeofによって抽出された関数loopFunc()のデータ型

 実は、前項で紹介したParametersやReturnTypeは、そのジェネリクスとして関数は指定できません。関数を記述すると、図7のエラーとなります。


図7 ジェネリクスに関数を指定してエラーとなった画面

 エラー文面にもあるように、ジェネリクスは型指定であって、関数は型ではないからです。そこで、これもエラー文面にあるように、typeof演算子が活躍します。既存の関数の引数の型や戻り値の型を抽出したい場合は、typeof演算子と組み合わせます。リスト18のように、typeofによって抽出された関数の型であるLoopFuncTypeを型指定とすると、問題なく動作します。

type LoopFuncTypeParams = Parameters<LoopFuncType>;
リスト18

 このコードで抽出されたLoopFuncTypeParams型は、図8の通り、リスト17の(1)の関数loopFuncの引数の型と一致します。


図8 typeofと組み合わせて既存の関数の引数の型を抽出したLoopFuncTypeParams

まとめ

 TypeScriptのTypeに注目し、あれこれ紹介する本連載はいかがでしたか?

 この連載中で紹介してきたように、TypeScriptには非常に強力な型システムがあり、さまざまな型指定や型定義、型操作ができます。これらを正しく使うことで、JavaScriptでは実現できなかった安心、安全なコーディングが可能となります。

 本連載の内容が、そのようなコーディング環境の一助となれば、これほどうれしいことはありません。

筆者紹介

WINGSプロジェクト

有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。

サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/
RSS(https://wings.msn.to/contents/rss.php
Twitter: @yyamada(https://twitter.com/yyamada
Facebook(https://www.facebook.com/WINGSProject


Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る