LoginSignup
2
1

More than 5 years have passed since last update.

TypeScript Handbook を読む (9. Type Compatibility)

Last updated at Posted at 2017-03-25

TypeScript Handbook を読み進めていく第九回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility (今ココ)
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Type Compatibility

原文

Introduction

TypeScript では、構造的部分型という考え方に基づいて型の互換性をチェックします。
構造的部分型は名目上の部分型と異なり、各型のメンバのみに基づいて互換性が判定されます。

TypeScript
interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// メンバが共通しているため、OK
p = new Person();

A Note on Soundness

TypeScript の型システムはコンパイル時に安全であると判断できない操作についても許容しています。
そのため、TypeScript がこのような不健全な振る舞いをする箇所について充分注意しておく必要があります。
この章ではそれがどのような場所で、なぜ発生するのかを説明していきます。

Starting out

TypeScript の構造的部分型の基本的なルールとして、y が少なくとも x と同じメンバを持つ場合に xy と互換性がある、とみなします。

TypeScript
interface Named {
    name: string;
}

let x: Named;
// y の型は { name: string; location: string; } とみなされる
let y = { name: "Alice", location: "Seattle" };
x = y;

関数呼び出し時の引数についても同じ規則が適用されます。

TypeScript
function greet(n: Named) {
    console.log("Hello, " + n.name);
}
greet(y); // OK

y は余分なメンバである location を持ちますが、互換性の確認時には対象の型 (この場合 Named) のメンバのみが考慮されるため、エラーになることはありません。

Comparing two functions

プリミティブ型やオブジェクト型の比較はそう難しくはありませんが、関数の型の比較となると若干複雑になります。

以下の例では、xy に代入できるか調べるためにまず引数リストを確認します。
この時、x の各引数の型は対応する y の引数と互換性がないといけませんが、引数の数は一致する必要はありません。
この例では x の各引数は y の各引数と互換性があるため、xy に代入することができます。
一方で、y の二番目の引数に対応する引数を x は持たないため、yx に代入することはできません。

TypeScript
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // エラー

y と同じ引数で x を呼び出しても単純に余分な引数を無視すれば良いだけだけど、逆に x と同じ引数で y を呼びだそうとしたら二番目の引数に渡せる値がないので NG というわけだ。

先ほどの例で y = x とした場合のように、引数の '破棄' をなぜ許容しているのか不思議に思うかもしれませんが、これは JavaScript において引数を破棄することが一般的なためです。
例えば、Array#forEach はコールバック関数に対して 3 つの引数を渡しますが、最初の引数だけを使用するコールバック関数でも充分役に立ちます。

TypeScript
let items = [1, 2, 3];

// 余分な引数を受け取りたくはない
items.forEach((item, index, array) => console.log(item));

// これでも OK!
items.forEach(item => console.log(item));

続いて戻り値の型の扱いについても見ていきましょう。
TypeScript では代入 の関数の戻り値の型は代入 の関数の戻り値の型の部分型である必要があります。

代入 にあるプロパティをすべて持っている必要があるということ

TypeScript
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});

x = y; // OK
y = x; // エラー。xの戻り値には location プロパティがない

Function Parameter Bivariance

代入元先どちらかの関数の引数がもう一方の関数の引数に代入可能であれば、それらの関数の引数の型は等しいとみなされます。
つまり、より具体的な型を引数に取る関数が指定されているにも関わらず、その部分型を引数に指定して関数を呼び出すことが可能ということであり、この仕様を不自然に思うかもしれません。

実際にはそれが問題となるようなケースは稀であり、また、これを許容することで JavaScript でよく用いられるパターンの多くを実現できるようになります。

TypeScript
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) {
    /* ... */
}

// MouseEvent を引数に取る関数に対し、Event を引数として呼び出すことになるため、
// 普通に考えるとまずいが、よく使われるパターンでもある
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));

Optional Parameters and Rest Parameters

関数の型を比較する時に、任意の引数と必須の引数はお互いに入れ替え可能です。
代入元の関数に余分な任意の引数があってもエラーになりませんし、代入先の関数にしかない任意の引数があってもエラーになりません。

また、可変長引数は無限個の任意の引数として扱われます。

これも不自然に聞こえるかもしれませんが、大抵の関数では任意の引数が指定されないことは、その引数に undefined が指定されていることと同義なため、問題になることはありません。

これが役に立つ例として、引数の数が (プログラマーにとっては) 予測可能だが、(コンパイラにとっては) 不明なコールバックを使用する例を見てみましょう。

TypeScript
function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* ... 'args' を引数に取るコールバックを呼び出す ... */
}

// invokeLater は任意の数の引数でコールバックを呼び出す "かも" しれないため、脆弱である
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

// (x と y は実際には必須であるため) 紛らわしく、引数の妥当性を検証することもできない
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));

Functions with overloads

関数がオーバーロードされている場合、代入元の関数のすべてのオーバーロードについて、代入先の関数と互換性がなければなりません。
これにより、常に代入元の関数と同じ条件で代入先の関数を呼び出すことができるようになります。

x("aaa")x(123) を許容していた場合、y = x とするためには yy("aaa")y(123) の両方に対応する必要があるということ

Enums

enum と数値型は互いに互換性があります。
しかし、他の enum とは互換性はありません。

TypeScript
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  // エラー

Classes

クラスはオブジェクトリテラルやインタフェースと同じように使用できますが、静的なメンバとインスタンスメンバを持つ点が異なります。
2 つのオブジェクトの型を比較する際には、静的なメンバは無視され、インスタンスメンバのみが比較の対象となります。

TypeScript
class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  //OK
s = a;  //OK

Private and protected members in classes

private/protected メンバを持つクラスのインスタンスを比較する場合、比較対象のクラスは同じクラスから派生した private/protected メンバを持つ必要があります。
そのため、自身の基底クラスに対して代入することは可能ですが、例え同じメンバを持っていても継承関係のないクラスには代入することはできません。

Generics

TypeScript は構造的部分型を採用しているため、型引数はメンバの型の一部として使用されない限り、型の互換性には影響を与えません。

以下の例では x も y もメンバに型引数を使用していないため、互換性があります。

TypeScript
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // OK。x と y の構造は同じ

型引数が指定されている場合、ジェネリックな型は非ジェネリックな型のように扱われます。

TypeScript
interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // エラー。x と y は互換性がない

ジェネリックな型の型引数が指定されていない場合、型引数に any が指定されているものとして比較が行われます。

TypeScript
let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // OK。(x: any)=>any は (y: any)=>any と互換性がある

Advanced Topics

Subtype vs Assignment

これまで "互換性がある" という言い方をしてきましたが、これは言語規約で規定されている用語ではありません。
TypeScript にはサブタイプ互換性と代入互換性という 2 種類の互換性が存在しており、その違いとは単に、代入互換性が any、および enum と (enum に対応する) 数値型の相互代入ができるようサブタイプ互換性を拡張したもの、という点だけです。

この 2 種類の互換性は状況に応じて使い分けられています。
実際のところ、implements 句や extends 句であっても代入互換性を基に型の互換性が決定されています。

詳細については TypeScript 仕様 を参照してください。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1