サイトトップ

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

HTML5テクニカルノート

TypeScript: 型の互換性


TypeScript公式Handbook「Type Compatibility」をもとにした解説です。TypeScriptの型は、データの構造にもとづいて決まります。型が合うか合わないかをどのように決めているのかご説明します。

01 型の基本的な決め方

TypeScriptで型が合うかどうかは、構造にもとづく型づけで定まります。備わるメンバーだけから型の結びつきを決めるのです。「構造型」(structural subtyping)と呼ばれ、「公称型」(nominal subtyping)と対比されます(「派生型」参照)。つぎのようなコードは、C#やJavaなどの公称型の言語ではエラーになります。クラスがインタフェースを実装していないからです。けれど、TypeScriptではメンバーと型が合うかどうかという構造型で型づけします。JavaScriptでは名前のない関数やオブジェクトリテラルなど、名前も明らかにしないことが多く、構造型になじみやすいからです。


interface IPoint {
	x: number;
	y: number;
}
class Vector {
	constructor(public x = 0, public y = 0) {}
}
let point: IPoint = new Vector();  // メンバーと型が互いに合う

TypeScriptの型づけは、コンパイルのときに安全だとわからない操作も、警告せずに許すことがあります。この点には注意しなければなりません。

02 代入と引数の型の互換性

つぎのコードは、インタフェースで型づけした変数に、オブジェクトリテラルが納められた変数を代入しています。このとき、オブジェクトがインタフェースのメンバーすべてをそれぞれ同じ型で備えていれば、代入は許されるのです。


interface IPoint {
	x: number;
	y: number;
}
let point: IPoint;
// {x: number, y: number, z: number}と推論される
let point3d = {x: Math.SQRT1_2, y: Math.SQRT1_2, z: Math.sqrt(3)};
point = point3d;

関数に引数を渡して呼び出すときも、同じように型がたしかめられます。引数のオブジェクトから参照されるプロパティが、同じ型で備わっていれば合っているとされるのです。使われないプロパティがあっても、エラーにはなりません。


function getLength(point: IPoint) {
	let square: number = point.x * point.x + point.y * point.y;
	return Math.sqrt(square);
}
console.log(getLength(point3d));  // 1

メンバーがオブジェクトで、さらにメンバーをもつときは、再帰的に調べられます。

03 関数の型づけを比べる

03-01 引数の型づけ

関数の型の比べ方は、少し変わってきます。引数の数が異なるふたつの関数で、考えましょう。引数の名前は問わず、型がたしかめられます。引数が代入先より少ないときは、エラーになりません。代入先の引数が足りない場合にエラーになります。必要な引数が渡せなくなるからです。他方、JavaScriptでは使わない引数は省いて関数が呼び出せます。そのため、引数が省かれた型の関数は代入できるのです。


let point3d = (x: number, y: number, z: number) => 0;
let polar = (radius: number, angle: number) => 0;

point3d = polar;
// polar = point3d;  // 代入先に引数が足りないのでエラー

Array.forEach()メソッドに渡すコールバック関数の引数が似た考え方です。引数は3つ受け取れます。けれど、第1引数の要素しか使わないことも多く、引数そのものを省いてしまう場合はよくあります。


let items = [0, 1, 2];
// 受け取った引数を使わない場合
items.forEach((item, index, array) => console.log(item));
// 使わない引数は省いてよい
items.forEach(item => console.log(item));

03-02 戻り値の型づけ

関数の戻り値についてもたしかめましょう。引数はなく、戻り値のメンバーの型は合っていて、数が違う場合です。このときは、構造にもとづく型づけで比べられます。代入先の関数の戻り値が備えるメンバーとその型がすべて合わなければなりません。足りなければエラーです。


let getOrigin = () => ({x: 0, y: 0});
let getOrigin3d = () => ({x: 0, y: 0, z:0});
getOrigin = getOrigin3d;
// getOrigin3d = getOrigin;  // 代入先のメンバーに足りないのでエラー

03-03 引数を関数で型づける

引数をコールバックなどの関数で型づけたとき、その関数の引数をクラスやインタフェースで型づけすると、それらを継承したサプクラスのインスタンスは渡せます。


enum EventType {Mouse, Keyboard}
interface Event {timestamp: number;}
interface MouseEvent extends Event {x: number; y: number;}
interface KeyEvent extends Event {keyCode: number;}
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
	/* ... */
}

けれど、関数本体でサブクラスだけがもつメンバーを参照することは、型づけのうえでは望ましくはありません。それでも、警告は出ませんし、JavaScriptではたびたび用いられる操作です。


listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y));

型を厳しく扱うためには、はっきりと型変換すべきでしょう。ただ、わずらわしさから、あまり好まれません。


listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + "," + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + "," + e.y)));

もちろん、まったく合わない型は許されず、エラーになります。


listenEvent(EventType.Mouse, (e: number) => console.log(e));  // 型が合わないのでエラー

関数の引数を比べるとき、多くの場合数の違いは問われません。省略できる引数ても、できなくても同じ扱いです。定められた引数より多く渡しても、足らなくてもエラーにはなりません。また、残余引数を用いると、数はかぎりがないものとみなされます。

型づけという意味では厳しさに欠けます。けれど、実行時のJavaScriptでは、関数に引数が与えられないことと、undefinedが渡されたのは同じ扱いです。

つぎのような関数も定められます。引数に配列と関数を受け取り、配列を引数としてコールバックします。関数に渡す配列要素の数や型、またコールバック関数の型は、呼び出す側はわかっています。けれど、コンパイラは、どのような呼び出しがされるかはたしかめられないでしょう。


function invokeLater(args: any[], callback: (...args: any[]) => void) {
	// 任意の数の引数argsでcallbackを呼び出す
	callback.apply(null, args);
}
invokeLater([0, 1, 2], (x, y, z) => console.log([x, y, z]));  // [ 0, 1, 2 ]
invokeLater(['hello', 'world!'], (a, b) => console.log(a + ', ' + b));  // hello, world!

さらに、省略可能な引数を関数に与えて呼び出してもとおります。ただ、きわめてわかりにくいことになるでしょう。


invokeLater([0, 1], (x, y?, z?) => console.log([x, y, z]));  // [ 0, 1, undefined ]

関数がオーバーロード(多重定義)している場合、当てはめるもとの関数に定めた型のすべてが、当てはめ先の型と合わなければなりません。つまり、ふたつの関数はすべての同じ呼び出し方ができるということです。

04 列挙型

列挙型と数値numberとは型が合います。けれど、他の列挙型との互換性は認められません。


enum Status {Ready, Waiting};
enum Color {Red, Blue, Green};
let selection = Status.Ready;
selection = 1;
console.log(Color.Blue);  // 1
// selection = Color.Blue;  // 列挙型が異なるのでエラー
let color: Color = 1;

05 クラス

クラスによる型づけはオブジェクト型リテラルやインタフェースとほぼ同じです(「TypeScript入門 04: オブジェクト型リテラルとインタフェースを使う」参照)。けれど、クラスは静的メンバーとインスタンスメンバーおよびコンストラクタを備えます。クラスの型が合うかを決めるのは、インスタンスメンバーとその型づけです。静的メンバーやコンストラクタは比べる対象には含まれないのです(コンストラクタは静的な定めとされます)。


class Point {
	constructor(public x = 0, public y = 0) {}
}
class Polar {
	x: number;
	y: number;
	static RAD_TO_DEG = 180 / Math.PI;
	constructor({radius = 0, angle = 0}) {
		this.x = radius * Math.cos(angle);
		this.y = radius * Math.sin(angle);
	}
}
let point: Point;
let polar = new Polar({radius: 2, angle: Math.PI / 3});
point = polar;
polar = new Point(0, 1);
console.log(polar, point);
// Point { x: 0, y: 1 } Polar { x: 1.0000000000000002, y: 1.7320508075688772 }

privateprotectedのインスタンスメンバーについては、属するクラスも同じでなければなりません。つまり、privateのメンバーをもつクラスで型づけすると、そのクラスかサブクラスのインスタンスしか合わないのです。

つぎのふたつのクラスPointとCoordsは、メンバーの名前と型づけ、およびprivate修飾子までまったく同じです。けれど、privateなメンバーの属するクラスが異なるので型は合いません。サブクラスVectorのインスタンスは親のprivateなメンバーを継承するため、型が合うとされるのです。


class Point {
	private x: number;
	private y: number;
	constructor(x: number, y: number) {
		this.x = x;
		this.y = y;
	}
}
class Vector extends Point {
	constructor(x: number, y: number) {
		super(x, y);
	}
}
class Coords {
	private x: number;
	private y: number;
	constructor(x: number, y: number) {
		this.x = x;
		this.y = y;
	}
}
let vector: Point = new Vector(3, 4);
// let coords: Point = new Coords(3, 4);  // privateプロパティの属するクラスが違うのでエラー

06 ジェネリック型

TypeScriptは構造型で比べますので、型引数はメンバーの型として使われなければ、区別されません。


interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y;  // 構造型は合っている

メンバーを型引数で型づけすると、その型が合うかどうか比べられます。異なる型引数を与えれば、構造型は異なります。一般の型指定をしたときと同じ扱いになるわけです。


interface NotEmpty<T> {
	data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
// x = y;  // メンバーが型づけされたので合わない

ジェネリック型に型引数を定めなければ、any型が与えられたものとみなされます。そのうえで、一般的な型づけと同じように比べられるのです。


let identity = function<T>(x: T): T {
	// ...
}
let reverse = function<U>(y: U): U {
	// ...
}
// つぎのふたつの型とみなされる
// (x: any) => any
// (y: any) => any
identity = reverse;


作成者: 野中文雄
作成日: 2017年4月11日


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