サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

TypeScript: 高度な型


TypeScriptには基本型に加え、より複雑な型を表す機能が備わっています。公式Handbook「Advanced Types」にもとづき、解説とコード例を大きく改めました。

01 交差型

交差型(Intersection Type)は、複数の型をひとつに合成します。すでにある型を組み合わせて、必要な機能の兼ね備わった型ができるのです。ひとつに合成された型には、もとの型のすべてのメンバーが含まれます。

交差型が用いられるのは、mixinなどこれまでのオブジェクト指向の枠に収まらない場合が多いでしょう。つぎのコード001は、mixinをつくる簡単な例です。関数(extend)が引数ふたつのオブジェクトを合成して、交差型の新たなオブジェクトとして返します。複数の型を結ぶのは記号&です。戻り値のオブジェクトは、もとのふたつのオブジェクトが備えるメソッドのどちらも呼び出せます。なお、**べき乗演算子です。

コード001■ふたつの引数のオブジェクトをmixinにより交差型オブジェクトにして返す関数


function extend<T, U>(first: T, second: U): T & U {
	const result = <T & U>{};
	for (let id in first) {
		(<any>result)[id] = (<any>first)[id];
	}
	for (let id in second) {
		if (!result.hasOwnProperty(id)) {
			(<any>result)[id] = (<any>second)[id];
		}
	}
	return result;
}
interface IPoint {
	x: number;
	y: number;
}
class Point implements IPoint {
	constructor(public x: number = 0, public y: number = 0) {}
	getLength(): number {
		return Math.sqrt(this.x ** 2 + this.y ** 2);
	}
}
class Polar implements IPoint {
	x: number;
	y: number;
	constructor(length: number = 1, angle: number = 0) {
		this.x = length * Math.cos(angle);
		this.y = length * Math.sin(angle);
	}
	getAngle(): number {
		return Math.atan2(this.y, this.x);
	}
}
const vector = extend(new Point(1, Math.sqrt(3)), new Polar());
console.log(vector.getLength(), vector.getAngle() * 180 / Math.PI);  // 1.9999999999999998 59.99999999999999

なお、前掲のコードはtsconfig.jsonのtargetをes6にすると、正しく動きません。クラスに定めたメソッドが、for...in文で取り出せないからです。

tsconfig.json

{
	"compilerOptions": {
		"target": "es6",

	}
}

02 共用型

共用型(Union Type)も複数の型をひとつにまとめます。けれど、交差型が複数の型のすべてのメンバーを備えるのに対して、共用型はいずれかひとつの型と一致すれば足ります。

つぎのコードは、共用型を使っていない関数の例です。数値を文字列にしたうえで、頭に0を加えて桁揃えします(コードの中身については「数字の頭に0を加えて桁揃えする4つのやり方」の「Array.prototype.join()メソッドで0を埋める」参照)。第1引数の数は文字列も受け取れるよう、anyで型づけしました。第2引数は揃える桁数です。anyは数値(number)と文字列(string)にかぎらずとおってしまうため、それ以外のオブジェクトなどを渡すと意図しない結果が返されてしまいます。


function setDigits(number: any, digits: number): string {
	let number_string = String(number);
	digits -= number_string.length;
	if (digits > 0) {
		const array: string[] = [];
		array[digits] = number_string;
		number_string = array.join('0');
	}
	return number_string;
}
console.log(setDigits(7, 3));  // 007
console.log(setDigits('7', 3));  // 007
console.log('result', setDigits({}, 3));  // [object Object]

typeof演算子などでデータの型を調べ、処理を分ければ目的の結果が得られるでしょう。つぎのコードの抜書きでは、数値と文字列以外を渡すと空文字列''が返されます。また、数値や文字列についても、適切でない値ははじきました。


function setDigits(number: any, digits: number): string {
	let number_string;
	switch (typeof number) {
		case 'number':
			if (isNaN(number)) {return '';}
			number_string = String(number);
			break;
		case 'string':
			const int = parseInt(number);
			if (isNaN(int)) {return '';}
			number_string = String(int);
			break;
		default:
			return '';
	}

}

さらに、関数の第1引数をnumberstringの共用型で定めれば、型チェックが働きます。複数の型を結ぶのは記号|です。共用型のデータから参照するメンバーは、すべての型に備わっていなければなりません。そうでないときは、<>などの構文による型変換で型を限定したうえで参照します。


// function setDigits(number: any, digits: number): string {
function setDigits(number: number | string, digits: number): string {

	switch (typeof number) {
		case 'number':
			// if (isNaN(number)) {return '';}
			if (isNaN(<number>number)) {return '';}

			break;
		case 'string':
			// const int = parseInt(number);
			const int = parseInt(<string>number);

			break;

	}

}

共用型で型づけし、typeof演算子によりデータの型を確かめるよう改めた関数がつぎのコード001です。数値(number)と文字列(string)以外を引数に渡すと、コンパイルエラーになります。

コード002■共用型とtypeof演算子によりデータ型を確かめるようにした関数


function setDigits(number: number | string, digits: number): string {
	let number_string;
	switch (typeof number) {
		case 'number':
			if (isNaN(<number>number)) {return '';}
			number_string = String(number);
			break;
		case 'string':
			const int = parseInt(<string>number);
			if (isNaN(int)) {return '';}
			number_string = String(int);
			break;
		default:
			return '';
	}
	digits -= number_string.length;
	if (digits > 0) {
		const array: string[] = [];
		array[digits] = number_string;
		number_string = array.join('0');
	}
	return number_string;
}
console.log(setDigits(7, 3));  // 007
console.log(setDigits('7', 3));  // 007
console.log(setDigits('1abc', 5));  // 00001
// console.log('result', setDigits({}, 3));  // コンパイルエラー

03 型ガードの関数を定める

前掲コード001のふたつのクラスを例にとりましょう。それぞれに、メソッドがひとつずつ定めてあります。ここで考えるのは、引数に渡したオブジェクトが備えるメソッドを呼び出す関数です。引数はふたつのクラスの共用型とします。


interface IPoint {
	x: number;
	y: number;
}
class Point implements IPoint {
	constructor(public x: number = 0, public y: number = 0) {}
	getLength(): number {
		return Math.sqrt(this.x ** 2 + this.y ** 2);
	}
}
class Polar implements IPoint {
	x: number;
	y: number;
	constructor(length: number = 1, angle: number = 0) {
		this.x = length * Math.cos(angle);
		this.y = length * Math.sin(angle);
	}
	getAngle(): number {
		return Math.atan2(this.y, this.x);
	}
}

このような場合、JavaScriptではメソッドのあるなしを調べてから呼び出す、という手法がよく用いられます。ただし、共用型で型づけしたとき気をつけるのは、前述のとおりすぺての型が備えているのでないメソッドを参照する場合は型変換しなければならないことです。


function callMethod(object: Point | Polar): number {
	if ((<Point>object).getLength) {
		return (<Point>object).getLength();
	} else if ((<Polar>object).getAngle) {
		return (<Polar>object).getAngle();
	}
}
const point = new Point(1, Math.sqrt(3));
const polar = new Polar(1, Math.PI / 3);
console.log(callMethod(point), callMethod(polar) * 180 / Math.PI);  // 1.9999999999999998 59.99999999999999

型ガード(Type Guard)という仕組みは、型を確かめたうえで、そのブロックのスコープ内は判別した型のデータとして扱うものです。そのため、ブロックの中では型変換はせずに済みます。型ガードを定めるのは、つぎの構文にもとづく関数です。


function isTypeA(parameter: TypeA | TypeB): parameter is TypeA {
	return ブール値;
}

前掲の関数は型ガードを用いて書き替えると、つぎのコード003のようになります。型ガードにより、関数本体のifelseブロック内は型変換が要らなくなりました。

コード003■型ガードを用いた関数


function isPoint(object: Point | Polar): object is Point {
	return Boolean((<Point>object).getLength);
}
function callMethod(object: Point | Polar): number {
	if (isPoint(object)) {
		return object.getLength();
	} else {
		return object.getAngle();
	}
}

04 typeof演算子による型ガード

前掲コード002の関数に型ガードを用いることもできます。その例がつぎのコードです。


function isNumber(value: number | string): value is number {
	return typeof value === 'number';
}
function setDigits(number: number | string, digits: number): string {
	let number_string;
	if (isNumber(number)) {
		if (isNaN(number)) {return '';}
		number_string = String(number);
	} else {
		const int = parseInt(number);
		if (isNaN(int)) {return '';}
		number_string = String(int);		
	}

}

もっとも、TypeScriptはtypeof演算子による型の評価は、つぎの場合には関数を定めなくても型ガードとみなして扱います。

したがって、前掲コード002は型ガード関数を使うことなく、つぎのように書き替えられます。第1引数が文字列(string)の場合も型ガードを働かせますので、else ifで等価比較を行わなければなりません。

コード004■typeof演算子で型ガードした関数


function setDigits(number: number | string, digits: number): string {
	let number_string;
	if (typeof number === 'number') {
		if (isNaN(number)) {return '';}
		number_string = String(number);
	} else if (typeof number === 'string') {
		const int = parseInt(number);
		if (isNaN(int)) {return '';}
		number_string = String(int);		
	}
	digits -= number_string.length;
	if (digits > 0) {
		const array: string[] = [];
		array[digits] = number_string;
		number_string = array.join('0');
	}
	return number_string;
}

05 instanceof演算子による型ガード

typeof演算子による評価は、オブジェクトの型ガードには使えませんでした。その場合に用いるのは、instanceof演算子です。オブジェクトのconstructor.prototypeプロパティのチェーンにコンストラクタのObject.prototypeが含まれるかどうかによって、型の一致を確かめます。instanceof演算子による型ガードで前掲コード003を書き直したのが、つぎのコード005てす。型ガード関数は要らなくなります。

コード005■instanceof演算子で型ガードした関数


function callMethod(object: Point | Polar): number {
	if (object instanceof Point) {
		return object.getLength();
	} else if (object instanceof Polar) {
		return object.getAngle();
	}
}

06 nullを受け入れる型

06-01 --strictNullChecksモード

TypeScriptには、特別なnull型とundefinedがあります。それぞれ、値はnullundefinedです。デフォルトでは、nullundefinedはすべての型に代入できます。それを避けるためのフラグは、--strictNullChecksです。

tsconfig.json

{
	"compilerOptions": {

		"strictNullChecks": true
	}
}

--strictNullChecksのモードでは、他の型で宣言された変数にはnullundefinedも値として納められません。これらの値を受け入れるためには、共用型で型づけにはっきりと含めなければならないのです。また、このモードはnullundefinedを異なる扱いとすることにご注意ください。共用型にnull型を加えていても、undefinedは与えられません。そのためには、undefined型も含めなければならないのです。


let pureString = 'string';
// pureString = null;  // コンパイルエラー
let stringIncludingNull: string | null = 'another string';
stringIncludingNull = null;  // OK
// stringIncludingNull = undefined;  // コンパイルエラー

06-02 オプションの引数とプロパティ

--strictNullChecksモードでは、オプションの引数は自動的にundefined型が加わった共用型になります。


function createPoint(x: number, y?: number) {
    return {x: x, y: y || 0};
}
console.log(createPoint(1, Math.sqrt(3)));  // {x: 1, y: 1.7320508075688772}
console.log(createPoint(1));  // {x: 1, y: 0}
console.log(createPoint(1, undefined));  // {x: 1, y: 0}
// console.log(createPoint(1, null));  // コンパイルエラー

これは、クラスのオプションのプロパティについても同じです。


class Point {
	constructor(public x: number, public y?: number) {
		this.y = y || 0;
	}
}
const point = new Point(1, Math.sqrt(3));
console.log(point);  // {x: 1, y: 1.7320508075688772}
console.log(new Point(1));  // {x: 1, y: 0}
console.log(new Point(1, undefined));  // {x: 1, y: 0}
// console.log(new Point(1, null));  // コンパイルエラー
point.y = undefined;
console.log(point);  // {x: 1, y: undefined}
// point.x = undefined;  // コンパイルエラー

06-03 型ガードと型変換

--strictNullChecksモードで共用型によりnullを含めたとき、値から除くには型ガードのコードを書き加えなければなりません。たとえば、つぎのようなJavaScriptコードです。


function getPureString(value: string | null): string {
	if (value == null) {
		return 'default';
	}
	else {
		return value;
	}
}

あるいは、論理演算子||を用いて、もっと簡単に書くこともできます(「if文なしに論理演算子で条件判定の処理をする」01「初期化されていない変数にデフォルト値を与える」参照)。


function getPureString(value: string | null): string {
	return value || 'default';
}

つぎのコードで入れ子の関数は、共用型でnullが含まれてる変数に対してメソッドを呼び出すために型変換しています。


function callEpithet(name: string | null): string {
	function postfix(epithet: string) {
		return (<string>name).charAt(0) + '. the ' + epithet;
	}
	name = name || 'Bob';
	return postfix('great');
}
console.log(callEpithet(null));  // B. the great

この場合、型変換演算子!をつぎの構文で用いることにより、識別子の型からnullundefinedが除けます。


識別子!


function callEpithet(name: string | null): string {
	function postfix(epithet: string) {
		return name!.charAt(0) + '. the ' + epithet;
	}
	name = name || 'Bob';
	return postfix('great');
}

前掲コードでコンパイラは、入れ子の関数に引数として明らかにnullでない値を渡して呼び出しても、型からnullが除けませんでした。入れ子の関数は、いつどこから呼び出されるか、すべてを捉えることができないからです。そのため、関数本体が実行されたときの識別子の型も限定できません。

07 型エイリアス

型エイリアス(Type Alias)は、型に新たな名前を与えるものです。インタフェースとも似ています。けれど、プリミティブや共用型、タプル型など、どのような型にもエイリアスはつくれます。宣言に用いるキーワードはtypeです。


type Numeric = number;
type NumericResolver = (x: number) => number;
type NumericOrResolver = Numeric | NumericResolver;
function calc(n: number, operation: NumericOrResolver) {
	if (typeof operation === 'number') {
		return n * operation;
	} else if (operation instanceof Function) {
		return operation(n);
	}
}
console.log(calc(2, 3));  // 6
console.log(calc(0, Math.cos));  // 1

型エイリアスは、新たな型をつくるのではありません。型を参照する新しい名前ができるだけです。プリミティブの型エイリアスは、さほど有用とはいえません。けれど、ドキュメントのかたちで使われることはあるでしょう。

インタフェースと同じく、ジェネリック型の型エイリアスもできます。型パラメータを加えて、エイリアス宣言の右辺に用いるだけです。


type Container<T> = {value: T};

定めた型エイリアスのプロパティを、そのエイリアスで型づけすることもできます。


type Tree<T> = {
	value: T;
	left: Tree<T>;
	right: Tree<T>;
};

さらに、交差型とともに用いると、複雑な型もつくれます。つぎの連結リストのコード006は、型エイリアスを交差型で定め、プロパティにそのエイリアスで型づけをしました。すると、そのプロパティのオブジェクトがまた同じ型エイリアスのプロパティをもつ、というように循環することになります。そこで、オブジェクトのクラスはコンストラクタが、オプションの引数にプロパティのオブジェクトを受け取ることにしました。前述06-02のとおり、オプションの引数はundefinedが受け取れます。そこで循環が切れるのです。

コード006■型エイリアスと交差型を用いた連結リスト


type LinkedList<T> = T & {next: LinkedList<T>};
interface Person {
	name: string;
}
class PersonsList implements LinkedList<Person> {
	public next: LinkedList<Person>;
	constructor(public name: string, next?: LinkedList<Person>) {
		this.next = next ? next : this;
	}
}
const people: LinkedList<Person> = new PersonsList(
	'Alice', new PersonsList(
		'Cheshire Cat', new PersonsList('Carroll')
	)
);
console.log(people.name);  // Alice
console.log(people.next.name);  // Cheshire Cat
console.log(people.next.next.name);  // Carroll
console.log(people.next.next.next.name);  // Carroll
console.log(people.next.next.next.next.name);  // Carroll

なお、つぎのような型エイリアスの宣言は、循環参照が切れないのでエラーになります。


// type Yikes = Array<Yikes>;  // 循環参照でエラー

この例について、Handbook「Advanced Types」の「Type Aliases」の項は、型宣言の右辺に型エイリアスは使えないとしています。けれど、型エイリアスを右辺に用いること自体は可能です。


type People = {name: string};
type Persons = Array<People>;
const persons: Persons = [
	{name: 'Alice'},
	{name: 'Cheshire Cat'},
	{name: 'Carroll'}
];

型エイリアスは、インタフェースと似たように使えます。けれど、異なる点もあります。型エイリアスは、継承や実装ができません。また、他の型を型エイリアスに継承・実装するといった仕組みも備わっていないのです。拡張に対して開かれたソフトウェアをつくるためには、できるだけインタフェースを用いるぺきでしょう。

他方で、共用型やタプル型を用いるとき、インタフェースで型を表現するのがむずかしい場合もあります。そのようなときには、型エイリアスを使ってください。

08 文字列リテラル型

文字列リテラル型(String Literal Type)は、文字列型のとるべき値を直に定めた型です。共用型と型ガードを組み合わせることで、決まった文字列だけを型の値として扱えます。文字列の値を使って列挙型のような処理ができるのです。

つぎのコード007は、文字列リテラル型で4つの矢印キー定めました。クラスのコンストラクタにキーの文字列を渡してつくったインスタンスは、その矢印キーが押されたかどうかを監視します。コンストラクタの引数に文字列リテラル型にない文字列を渡すと、コンパイルエラーになります。

コード007■文字列リテラル型で定めた矢印キーが押されたかどうかを監視する


type ArrowKey = 'left' | 'right' | 'up' | 'down';
class InspectArrowKey {
	keyName: string;
	keyCode: number;
	constructor(key: ArrowKey) {
		if (key === 'left') {
			this.inspect(key, 37);
		} else if (key === 'up') {
			this.inspect(key, 38);
		} else if (key === 'right') {
			this.inspect(key, 39);
		} else if (key === 'down') {
			this.inspect(key, 40);
		}
	}
	inspect(keyName:string, keyCode: number) {
		this.keyName = keyName;
		this.keyCode = keyCode;
		document.addEventListener('keydown', (event) => {
			if (event.keyCode === this.keyCode) {
				console.log(this.keyName, this.keyCode);
			}
		});
	}
}
const inspectLeft = new InspectArrowKey('left');
const inspectRight = new InspectArrowKey('right');
// const inspectShift = new InspectArrowKey('shift');  // コンパイルエラー

文字列リテラル型は、オーバーロードする関数を分けるために使うこともできます。つぎの関数は文字列の引数を文字列リテラル型で定めました。


function createElement(tagName: 'img'): HTMLImageElement;
function createElement(tagName: 'input'): HTMLInputElement;
// ... 必要があれば追加 ...
function createElement(tagName: string): Element {
	const element = document.createElement(tagName);
	return element;
}
console.log(createElement('img'));  // <img>
console.log(createElement('input'));  // <input>
// console.log(createElement('div'));  // コンパイルエラー

09 数値リテラル型

数値についても、数値リテラル型(Numeric Literal Type)があります。数値型のとるぺき値そのものを、数値で型として与えられるのです。


type DiceNum = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): DiceNum {
	const randomInt  = <DiceNum>(Math.floor(Math.random() * 6) + 1);
	return randomInt;
}

数値リテラル型は、暗黙で働くこともあります。まず、つぎのコードは働かない場合です。関数本体のif条件の式に、論理演算子||が用いられています。けれど、右の式は意味がありません。左の式がfalseと評価されたとき、右の式はつねにfalseだからです。


function compare(x: number) {
	if (x > 1 || x > 2) {
		//
	}
}

以下のコードでは、うえの例の比較演算子(>)をそれぞれ不等価(!==)と等価(===)に替えました。やはり、右の式は意味がありません。それだけでなく、右の式についてコンパイルエラーが起こります(図001)。エラーメッセージは、つぎのとおりです。左の式がfalseと評価されると変数は数値リテラル型1と推論され、2とは比べられないということです。

演算子 '===' を型 '1' および '2' に適用することはできません。

function compare(x: number) {
	if (x !== 1 || x === 2) {
		//
	}
}

図001■if条件の右の式がコンパイルエラーになる

図001

10 列挙型メンバーの型

列挙型(enum)は、メンバーすべてがリテラルで初期値を与えられている場合には、その値は型として扱われます。詳しくは、「Enums」の「Union enums and enum member types」の項をお読みください。


enum KeyEvent {
	keyUp = 'keyup',
	keyDown = 'keydown'
}
function InspectKeyDown(keyEvent: KeyEvent.keyDown) {
	document.addEventListener(keyEvent, (event) => {
		console.log(event.keyCode);
	});
}
InspectKeyDown(KeyEvent.keyDown);
// InspectKeyDown(KeyEvent.keyUp);  // コンパイルエラー

11 判別共用型

リテラル型に共用型、さらに型ガードと型エイリアスを組み合わせると、判別共用型(Discriminated Union)という応用的な型の仕組みがつくれます(判別共用体、あるいは代数的データ型タグ付き共用型などとも呼ばれる考え方です)。関数型プログラミングにとくに役立ちます。TypeScriptは、JavaScriptの構造にもとづいて判別共用型を組み込みました。つぎの3つの要素から成り立ちます。

  1. 共通のリテラル型プロパティを備えた判別のための型
  2. 判別用の型からなる共用型の型エイリアス
  3. 共通のプロパティの型ガード

まず定めるのはインタフェースで、それをつぎに共用型にまとめるという手順です。インタフェースには共通のプロパティを加え、判別のための異なる文字列リテラル型が与えられます。このプロパティは判別用のタグとも呼ばれます。インタフェースのその他のプロパティは、それぞれ違って構いません。つまり、インタフェースは同じ名前のプロパティがあるほかは、互いに何も関わりはないのです。そこで、これらのインタフェースを共用型にまとめて、型エイリアスを定めます。


type Shape = Square | Rectangle | Circle;
interface Square {
	kind: 'square';
	size: number;
}
interface Rectangle {
	kind: 'rectangle';
	width: number;
	height: number;
}
interface Circle {
	kind: 'circle';
	radius: number;
}

それでは、インタフェースに共通に与えたプロパティに対して、型ガードの処理を加えましょう。関数の引数は、共用型の型エイリアスで型づけします。そして、switch文で共通のプロパティの型を確かめればよいのです。プロパティの文字列リテラル型に与えられていない文字列と比較しようとすれば、コンパイルエラーになります。


function area(shape: Shape) {
	switch (shape.kind) {
		case 'square':
			return shape.size ** 2;
		case 'rectangle':
			return shape.height * shape.width;
		case 'circle':
			return Math.PI * (shape.radius ** 2);
		// case 'triangle':  // コンパイルエラー
			// return shape.base * shape.height / 2;
	}
}
const square: Square = {kind: 'square', size: 1}
const circle: Circle = {kind: 'circle', radius: 1}
console.log(area(square), area(circle));  // 1 3.141592653589793

12 型チェックの徹底

前項のコード例に、新たなインタフェースを加え、共用型に含めてみます。


type Shape = Square | Rectangle | Circle | Triangle;

interface Triangle {
	kind: 'triangle';
	base: number;
	height: number;
}

すると、関数本体の型ガードで、switch文が判別共用型のすべてのcaseを拾いきれていません。つまり、関数の戻り値にundefinedが含まれるということです。--strictNullChecksモードであれば、戻り値に型を与えることによりコンパイルエラーで確かめられます。


function area(shape: Shape): number {  // undefinedはコンパイルエラー
	switch (shape.kind) {
		case 'square':
			return shape.size ** 2;
		case 'rectangle':
			return shape.height * shape.width;
		case 'circle':
			return Math.PI * (shape.radius ** 2);
		// case 'triangle': がない
	}
}

--strictNullChecksモードでなくても使えるのがneverです。取り得るすべての型が除かれた型を表します。条件分岐で取りこぼしがあると、実際にある型が残されてしまいます。すると、never型で受け入れられないために、コンパイルエラーが起こるのです(図002)。never型判別の関数を新たに加えなければならないものの、拾い漏れをよりはっきりと確かめられます。コード全体は、以下のコード008にまとめました。


function area(shape: Shape): number {
	switch (shape.kind) {

		default: return assertNever(shape);
	}
}
function assertNever(object: never): never {
    throw new Error('Unexpected object: ' + object);
}

図002■拾い漏れた型があるとコンパイルエラーになる

図002

コード008■判別共用型のコードで型チェックを徹底する


type Shape = Square | Rectangle | Circle | Triangle;
interface Square {
	kind: 'square';
	size: number;
}
interface Rectangle {
	kind: 'rectangle';
	width: number;
	height: number;
}
interface Circle {
	kind: 'circle';
	radius: number;
}
interface Triangle {
	kind: 'triangle';
	base: number;
	height: number;
}
function area(shape: Shape): number {
	switch (shape.kind) {
		case 'square':
			return shape.size ** 2;
		case 'rectangle':
			return shape.height * shape.width;
		case 'circle':
			return Math.PI * (shape.radius ** 2);
		case 'triangle':
			return shape.base * shape.height / 2;
		default: return assertNever(shape);
	}
}
function assertNever(object: never): never {
    throw new Error('Unexpected object: ' + object);
}

13 多態なthis型

多態なthis型(Polymorphic this type)というのは、クラスやインタフェースを継承する型のことです(F-bounded polymorphismと呼ばれます)。階層をつなぐインタフェースが簡単につくれます。たとえば、つぎのクラスのメソッドは、プロパティの座標計算のあとインスタンスを返します。すると、メソッドをつなげて呼び出すことができるのです。


class Point {
	constructor(public x: number = 0, public y: number = 0) {}
	get length(): number {
		return Math.sqrt(this.x ** 2 + this.y ** 2);
	}
	add(x: number, y: number): Point {
		this.x += x;
		this.y += y;
		return this;
	}
	scale(scale: number): Point {
		this.x *= scale;
		this.y *= scale;
		return this;
	}
}
const point = new Point(1)
.add(0, Math.sqrt(3))
.scale(1/2);
console.log(point.length);  // 0.9999999999999999

ただし、このクラスを継承すると問題が生じます。スーパークラスのメソッドが返すのは親の型のインスタンスなので、サブクラスのメソッドが呼び出せないのです。


class Polar extends Point {
	static get(length: number = 1, angle: number = 0) {
		return new Polar(
			length * Math.cos(angle),
			length * Math.sin(angle)
		);
	}
	rotate(angle: number): Polar {
		const sin = Math.sin(angle);
		const cos = Math.cos(angle);
		this.x = this.x * cos - this.y * sin;
		this.y = this.x * sin + this.y * cos;
		return this;
	}
}
const point = Polar.get()
.add(0, Math.sqrt(3))
.rotate(Math.PI / 3);  // コンパイルエラー

そこで、thisを返すメソッドは、戻り値をthisで型づけします。すると、サブクラスのインスタンスがスーパークラスのメソッドを呼び出しても、自分のクラスの型のインスタンスが得られるのです。以下のコード009は、メソッドの戻り値をthis型に改めました。すると、たとえばつぎのように、親クラスのメソッドから返されたインスタンスに対して、自分のクラスのメソッドが呼び出せます。


const point = Polar.get()
.add(0, Math.sqrt(3))
.rotate(Math.PI / 3)
.scale(1 / 2);
console.log(point.length, point.angle * 180 / Math.PI);  // 1 120.00000000000001

コード009■戻り値がthis型のスーパークラスのメソッドから自クラスで型づけされたインスタンスを得る


class Point {
	constructor(public x: number = 0, public y: number = 0) {}
	get length(): number {
		return Math.sqrt(this.x ** 2 + this.y ** 2);
	}
	add(x: number, y: number): this {
		this.x += x;
		this.y += y;
		return this;
	}
	scale(scale: number): this {
		this.x *= scale;
		this.y *= scale;
		return this;
	}
}
class Polar extends Point {
	static get(length: number = 1, angle: number = 0) {
		return new Polar(
			length * Math.cos(angle),
			length * Math.sin(angle)
		);
	}
	get angle(): number {
		return Math.atan2(this.y, this.x);
	}
	rotate(angle: number): this {
		const x = this.x;
		const y = this.y;
		const sin = Math.sin(angle);
		const cos = Math.cos(angle);
		this.x = x * cos - y * sin;
		this.y = x * sin + y * cos;
		return this;
	}
}

14 インデックス型

インデックス型(Index type)を用いると、プロパティ名を動的に使うコードがコンパイラでチェックできます。そのために用いる演算子を先にご紹介しましょう。

まず、インデックス型クエリ演算子keyofです。オペランド(被演算子)の型が備えるpublicのプロパティ名を文字列リテラル型としてまとめた共用型を返します。もとの型のプロパティを取り出して、自動的に文字列リテラルの共用型がつくられるということです。インデックス型クエリ演算子は、ジェネリック型のクラスや関数にも使えます。引数に渡されたオブジェクトが、プロパティを実際に備えているかどうか確かめられるのです。


interface Person {
	name: string;
	age: number;
}
let personProps: keyof Person;  // 'name' | 'age'
personProps = 'name';
// personProps = 'sex';  // コンパイルエラー

つぎに、インデックスアクセス演算子[]です。型からプロパティを参照し、その型が返されます。

型[プロパティ名]

let string: Person['name'] = 'Theta';
let number: Person['age'] = 12;
// let error: Person['age'] = true;  // コンパイルエラー

インデックスアクセス演算子[]は、インデックス型クエリ演算子keyofとともにジェネリック型に用いられることで真価が発揮されます。その場合、型変数にextendsキーワードを添えることにより、keyofの型が拡張して加わるようにします。つぎの関数は、オブジェクトからプロパティ名で取り出した値を返すため、戻り値の型はインデックスアクセス演算子[]で定めています。第2引数に渡すプロパティ名によりこの型が変わりますので、それに応じて戻り値のデータ型も異なることになるのです。第1引数のオブジェクトにないプロパティを第2引数に渡せば、コンパイルエラーになります。


interface Person {
	name: string;
	age: number;
}
let person: Person = {
	name: 'Alice',
	age: 7
};
function getProperty<T, K extends keyof T>(object: T, name: K): T[K] {
	return object[name];
}
console.log(getProperty(person, 'name'));  // Alice
console.log(getProperty(person, 'age'));  // 7
// getProperty(person, 'sex'); // コンパイルエラー

オブジェクトから複数のプロパティを取り出す関数に、インデックスで型づけしたのが以下のコード010です。第2引数には、つぎのようにプロパティ名の配列を渡します。プロパティは、Array.map()メソッドで新たな配列にして返しています。


const properties: (string | number)[] = pluck(person, ['name', 'age']);
console.log(properties);  // ["Alice", 7]
// pluck(person, ['sex']); // コンパイルエラー

コード010■インデックスで型づけしたオブジェクトからプロパティを取り出して返す関数


interface Person {
	name: string;
	age: number;
}
let person: Person = {
	name: 'Alice',
	age: 7
};
function pluck<T, K extends keyof T>(object: T, names: K[]): T[K][] {
	return names.map(n => object[n]);
}

インタフェースにインデックスアクセス演算子[]でプロパティを定めると、任意の名前でプロパティが加えられます(04「余分なプロパティの扱い」)。このとき、インタフェースにkeyof演算子を用いるとプロパティ名(キー)の型が、[]演算子で任意のプロパティ名を渡すとその値の型が得られるのです。


interface Map<T> {
	[key: string]: T;
}
let keys: keyof Map<number> = 'name'; // string
let value: Map<number>['property'] = 10; // number

15 型のマップ

ある型にもとづいて新たな型をつくるのが型のマップ(Mapped type)です。つぎのインタフェースから、マップによりプロパティの定めに少し手の加わった新たな型をつくってみましょう。


interface Person {
	name: string;
	age: number;
}

まず、プロパティのあとに?を添えると、そのプロパティが省けるようになります(02「省けるプロパティを定める」)


interface PersonPartial {
	name?: string;
	age?: number;
}

この型はインデックスを用いたマップにより、つぎのようにつくることができるのです。プロパティ名とその型は、マップ先の型にそのまま移されます。そこに、省略可能の定めが新たに加えられたのです。


type TPartial<T> = {
	[P in keyof T]?: T[P];
};
let personPartial: TPartial<Person> = {name: 'Alice'};
console.log(personPartial);  // {name: "Alice"}

つぎに、プロパティを読み取り専用にする場合です。このときは、プロパティの前に修飾子readonlyを添えます。


interface PersonReadonly {
	readonly name: string;
	readonly age: number;
}

この型も、インデックスでマップすればつくれます。つぎのようにreadonly修飾子をマップに加えればよいのです。


type TReadonly<T> = {
	readonly [P in keyof T]: T[P];
};
let personReadonly: TReadonly<Person> = {name: 'Alice', age: 7};
console.log(personReadonly);  // {name: "Alice", age: 7}
// personReadonly.name = 'Theta';  // コンパイルエラー

これらふたつの型のマップは便利なので、実はTypeScriptの標準ライブラリにそれぞれキーワードPartialReadonlyとして備わっています。


type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

型をマップするためのもっとも基本的な構文は、for...in文と似ています。つぎの3つの要素で構成され、以下がコードの例です。

  1. マップするプロバティを取り出すもととなる文字列リテラルの共用型(Keys)
  2. 順に取り出すプロパティを納めるための型変数(K)
  3. プロパティをマップして定めた新たな型(Flags)

type Keys = 'option1' | 'option2';
type Flags = {[K in Keys]: boolean};
let options: Flags = {option1: true, option2: false};

この例では、共用型の文字列リテラルをキーワードinにより、プロパティ名として取り出しました。そのうえで、いずれもbooleanを型に与えたのです。したがって、つぎの型と同じ定めになります。


type Flags = {
	option1: boolean;
	option2: boolean;
};

もっと実際的なのは、前述のようにすでにある型をもとにすることです。その場合、keyof演算子でプロパティ名を取り出し、[]演算子により型づけするのです。つぎのコードは、--strictNullChecksモードでもプロパティにnullが与えられる型をつくります。


type NullablePerson = { [P in keyof Person]: Person[P] | null }
let person: NullablePerson = {name: 'Alice', age: null};
// let person: Person = {name: 'Alice', age: null};  // コンパイルエラー

PartialReadonlyのようにジェネリック型にすれば、より使い勝手は増すでしょう。


type Nullable<T> = {[P in keyof T]: T[P] | null};

うえの例では、keyof演算子によりプロパティのリストを取り出し、[]演算子で得た型に手を加えました。これは、使い回しのきく型のマップの仕方です。マップするときに、もとのプロパティと型の構造を崩しません。このような変換は準同型と呼ばれます(「準同型写像」参照)。新たな型はもとのプロパティにもとづいてつくられ、読み取り専用や省略可能の定めもそのまま引き継がれるのです。

以下のコード011に定めた関数は、引数に受け取ったオブジェクトのプロパティを別に定めた型でラップして返します。その結果、戻り値の新たなオブジェクトには、つぎのようにプロパティにget()/set()メソッドが備わるのです。


let props = {name: 'Alice', age: 7};
let proxyProps = proxify(props);
console.log(proxyProps.name.get(), proxyProps.age.get());  // Alice 7
proxyProps.name.set('Theta');
proxyProps.age.set(12);
console.log(proxyProps.name.get(), proxyProps.age.get());  // Theta 12

コード011■引数に受け取ったオブジェクトのプロパティを別に定めた型でラップして返す関数


type Proxy<T> = {
	get(): T;
	set(value: T): void;
}
type Proxify<T> = {
	[P in keyof T]: Proxy<T[P]>;
};
function proxify<T>(o: T): Proxify<T> {
	let result = {} as Proxify<T>;
	for (const k in o) {
		result[k] = {
			get() {return o[k]},
			set(value) {o[k] = value;}
		};
	}
	return result;
}

PartialReadonlyのほかにも、TypeScriptのライブラリには型を変換する機能が備わっています(「Partial, Readonly, Record, and Pick」)。ひとつは、Pickです。もとの型からプロパティの一部を取り出して新たな型にします。PartialReadonlyと同じく、もとの型の構造を崩さない準同型です。


let personName: Pick<Person, 'name'> = {name: 'Alice'};
// let personName: Pick<Person, 'name'> = {name: 'Alice', age: 7};  // コンパイルエラー
let personNameAge: Pick<Person, 'name' | 'age'> = {name: 'Alice', age: 7};

もうひとつはRecordで、同じ型のプロパティを複数定めます。与えるプロパティ名は文字列リテラルの共用型です。もとになるプロパティをもつ型がないので、準同型の変換ではありません。


let stringProps: Record<'prop1' | 'prop2', string> = {prop1: 'one', prop2: 'two'}

なお、PickRecordの実装を示すと、つぎのとおりです。


type Pick<T, K extends keyof T> = {
	[P in K]: T[P];
};
type Record<K extends string, T> = {
	[P in K]: T;
};

前掲コード011のオブジェクトをラップして返す関数の変換は準同型です。したがって、もとの型のオブジェクトに戻すことも簡単にできます。その関数が以下のコード012です。つぎのようにして試せます。


let props = {name: 'Alice', age: 7};
let proxyProps = proxify(props);
let originalProps = unproxify(proxyProps);
console.log(originalProps);  // {name: "Alice", age: 7}

コード012■ラップされたオブジェクトからもとの型のオブジェクトを再現する関数


function unproxify<T>(t: Proxify<T>): T {
	let result = {} as T;
	for (const k in t) {
		result[k] = t[k].get();
	}
	return result;
}


作成者: 野中文雄
作成日: 2018年5月1日


Copyright © 2001-2018 Fumio Nonaka.  All rights reserved.