HTML5テクニカルノート
TypeScript: 型の互換性
- ID: FN1704004
- Technique: HTML5 / JavaScript
- Package: TypeScript 2.2
TypeScript公式Handbook「Type Compatibility」をもとにした解説です。TypeScriptの型は、データの構造にもとづいて決まります。型が合うか合わないかをどのように決めているのかご説明します。かいつまんだ内容が知りたいという場合には「TypeScript入門 08: 型の互換性」をお読みください。
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 }
private
やprotected
のインスタンスメンバーについては、属するクラスも同じでなければなりません。つまり、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年5月31日「TypeScript入門 08: 型の互換性」の参照を追加。
作成日: 2017年4月11日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.