高等な型

交差型(Intersection Types)

交差型(intersection type)は複数の型を1つに連結したものです。 これは既存の型を統合し、必要とする機能を全て備えた1つの型を得ることを可能にしてくれます。

例えば、Person & Serializable & Loggableは、PersonSerializableLoggableです。 これは、この型のオブジェクトが3つの型の全てのメンバを持つことを意味します。

あなたは、ミックスインやその他の古典的なオブジェクト指向に囚われない処理上で、交差型を見かけることになるでしょう。 (こういったことはJavaScriptには非常に多いです!) 下記の例は、ミックスインの作り方を示しています。

function extend<T, U>(first: T, second: U): T & U {
    let 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;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

共用体型(Union Types)

共用体型(Union types)は交差型と密接な関係がありますが、これらの使用方法は全く異なります。 時に、数値または文字列どちらかのパラメーターを期待するライブラリを実行することがあるでしょう。 例えば、下記の関数があるとします。

/**
 * 文字列を取得し、"padding"を左に付け加えます。
 * もし、'padding'が文字列であれば、左側にそれを付け加えます。
 * もし、'padding'が数値であれば、左側にその数値分の空白を付け加えます。
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

padLeftの問題は、paddingパラメーターの型がanyであることです。 これは引数に数値でも文字列でも無いものも指定して呼び出すことが可能であることを意味しますが、 TypeScriptではこれは問題無いとして扱います。

// コンパイルは通りますが、ランタイム時に失敗します
let indentedString = padLeft("Hello world", true);

伝統的なオブジェクト指向のコードであれば、型の階層構造を作ることで2つの型を抽象化しようとするかもしれません。 これは明確な対処方法ではありますが、少しやり過ぎなところもあります。

padLeftの元のバージョンの素晴らしいことの1つは、プリミティブ値を渡すだけで良いことです。 使用することに関して言えば、シンプルで簡潔であると言えるでしょう。 この新しいアプローチでも、既に他の場所で関数が使用されていた場合には、それを手助けすることはできないでしょう。

そこでanyの代わりに、paddingパラメーターのために共用体型(union type)を使用します。

/**
 * 文字列を取得し、"padding"を左に付け加えます。
 * もし、'padding'が文字列であれば、左側にそれを付け加えます。
 * もし、'padding'が数値であれば、左側にその数値分の空白を付け加えます。
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

// コンパイル時にエラー
let indentedString = padLeft("Hello world", true);

共用体型(union types)は値が幾つかの型のうちの一つであることを表します。 各型を縦棒(|)で区切ます。 number | string | booleanであれば、その値は数値、文字列、真偽値になることが可能です。

共用体型(union types)の値を持つ場合、全ての型で共通するメンバにしか、アクセスすることはできません。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // OK
pet.swim();    // エラー

ここでの共用体型(union types)は少しトリッキーかもしれませんが、慣れるためには少し直感力が必要かもしれません。 もし値がA | Bを持つ場合、そのメンバはAとBの両方で共通するメンバを持つことのみが確定されます。

この例では、Birdflyというメンバを持ちますが、 Bird | Fishとしての変数の型が、flyメソッドを持つことは確定されません。 もし、変数がランタイム時にはFishである場合、pet.fly()の呼び出しは失敗します。

型の保護と型の識別(Type Guards and Differentiating Types)

共用体型(union types)は、値が取り得ることが出来る型を重ね持つ際に、それをモデリングする際に便利です。 ただし、Fishを持つのかどうかを明確に知る必要がある場合に、どうすればよいのでしょうか?

値が2つの型のどちらかの可能性がある場合にそれを識別するには、一般的なJavaScriptであれば提供されるメンバを調べます。 既に述べたようにアクセス可能なメンバは、共用体型(union types)の全てで構成要素となっていることが保証されているメンバだけです。

let pet = getSmallPet();

// 各プロパティへのアクセスはエラーを引き起こします
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

同じコードで動くようにするには、型注釈(type assertion)を使う必要があります。

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

ユーザー定義による型の保護

この場合、あるゆる場所で型注釈(type assertion)を使わなければいけません。 理想を言えば、型のチェックを一度行ったのであれば、その各分岐部分ではpetの型であることが知られるようになることです。

TypeScriptにはこのような仕組みがあり、これは「型の保護(type guard)」と呼ばれています。 型の保護は、あるスコープ内で型が保証されることをチェックする、ランタイム時に実行される式になります。 型の保護を定義するには、単純に「型述語(type predicate)」を返す関数を定義する必要があります。

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

pet is Fishはこの例での型述語になります。 述語はparameterName is Typeの形式をとり、 parameterNameは現在の関数に指定されているパラメーターと同じ名前にしなければいけません。

isFishが値を指定されて呼び出されると、元の型と互換性があれば、 TypeScriptはその変数を特定の型に狭めてくれます。

// 'swim'と'fly'のどちらの呼び出しもOK

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

TypeScriptは、そのif部分の中でpetFishであることを知るだけで無く、 else部分の中ではFishでは無いことから、Birdであることも知ることが出来ることに注意してください。

typeofによる型の保護

padLeftに戻って、共用体型(union types)を使用したバージョンにコードを書き直してみましょう。 次のように、型述語を使用して書いてみました。

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

しかしながら、型がプリミティブであるかを調べる関数を定義しなければいけないというのは、少々違和感があるでしょう。 幸い、typeof x === "number"を関数内で抽象的にする必要はありません。 何故なら、TypeScriptは型の保護があることを自分自身で理解してくれるからです。 つまり、インラインでチェックする処理を書くことが可能であるということです。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

これらのtypeofによる型の保護は、 typeof v === "typename"typeof v !== "typename"の異なる2つの形式を認識し、 "typename""number""string""boolean""symbol"のいずれかでなければいけません。 TypeScriptは他の文字列と比較することを止めるようなことはしませんが、 型の保護としての認識は行ってくれません。

instanceofによる型の保護

もし、typeofによる型の保護に目を通しており、JavaScriptのinstanceof演算子に慣れ親しんでいれば、 このセクションの内容の予想がついているかもしれませんね。

instanceofによる型の保護はコンストラクタ関数を使用して、型を狭める方法です。 例として、先程の産業用向けの文字列パディング(string-padder)を拝借して試してみましょう。

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 型は、'SpaceRepeatingPadder | StringPadder'
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 'SpaceRepeatingPadder'に狭められた型
}
if (padder instanceof StringPadder) {
    padder; // 'StringPadder'に狭められた型
}

instanceofの右側はコンストラクタ関数でなければならず、 TypeScriptは下記の順で次のように処理します。

  1. 関数のprototypeのプロパティがanyでなければ、その型に狭め、
  2. その型のコンストラクタ・シグネチャによって返される型の共用体(union)に狭めます。(翻訳に自信なし)

Nullの型(Nullable types)

TypeScriptには特別な型nullundefinedが存在し、それぞれnullとundefinedの値を持ちます。 これらは、基本の型のセクションで概要を少しだけ説明しています。

デフォルトでは、型チェッカーはnullundefinedはどんなものにも割り当て可能であるとみなします。 実際に、nullundefinedはあらゆる型で有効な値です。

つまり、例えそれを禁止にしたいとしても、どの型にも割り当てられないようにすることはできません。 Tony Hoare氏は、これを10億ドルに相当する誤りと呼んでいます。

--strictNullChecksフラグはこれを正すもので、変数の宣言時に自動的にnullまたはundefinedを含めません。 共用体型(union type)を使用して、明示的にそれを含めることが可能です。

let s = "foo";
s = null; // error, 'null' is not assignable to 'string'
let sn: string | null = "bar";
sn = null; // ok

sn = undefined; // error, 'undefined' is not assignable to 'string | null'

TypeScriptはJavaScriptのセマンティクスと一致させるために、nullundefinedを別々に扱うことに注意してください。 string | nullは、string | undefinedstring | undefined | nullと異なる型になります。

任意のパラメーターとプロパティ

--strictNullChecksを使用すると、任意のパラメーターに自動的に| undefinedが追加されます。

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

任意のプロパティも同様です。

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

型の保護(type guard)と型のアサーション

Nullの型(nullable types)は共用体(union)を使用して実装できるため、nullを取り除くには型の保護を使用する必要があります。 幸いなことに、これはJavaScriptで書かれるコードと同じコードです。

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

これでnullを明確に排除できていますが、次のように演算子を使用して返しても問題ありません。

function f(sn: string | null): string {
    return sn || "default";
}

コンパイラがnullまたはundefinedを取り除くことができない場合は、 型注釈(type assertion)の演算子を使用して手動で削除することができます。 接頭辞の!は、型からnullundefinedを取り除きます。

// 誤
function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

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

この例でネストされた関数を使用しているのは、コンパイラがネストされた関数内部でnullを取り除くことができないためです。 (即時実行関数を除く)

これは入れ子にされた関数の呼び出し全てを追跡できないためです。 特に外部の関数から、それを戻り値として返す場合に追跡が非常に困難になります。 関数が呼び出される場所が分からなければ、関数本体の実行時にどのような名前の型になるのかを知ることはできません。

型のエイリアス(Type Aliases)

型のエイリアスは、型に対する新しい名前を作成します。 型のエイリアスはインターフェースと似ているところがありますが、 プリミティブ、共用体(union)、タプル、その他にあなたの手によって書かれた型に対して名前を付けることが可能です。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === "string") {
        return n;
    }
    else {
        return n();
    }
}

エイリアスは実際に新しい型を作成するわけではなく、その型を参照する新しい名前を作成するだけです。 プリミティブへのエイリアスはそれほど有用ではありませんが、ドキュメントの形式上これを使って説明します。

インターフェースのように、エイリアスもジェネリクスになることも可能です。 型パラメーターを追加し、エイリアス定義の右側でそれを使用します。

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

また、プロパティの中で自身を参照する型のエイリアスを持つことも可能です。

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

交差型と組み合わせることで、非常に奇妙で理解し難い(mind-bending)型を作成することが出来ます。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

ただし、型のエイリアスを定義の右側のいずれの場所にも表すことはできません。

type Yikes = Array<Yikes>; // error

インターフェース vs 型のエイリアス

既に述べたように、型のエイリアスはインターフェースのように振る舞うことができますが、 幾つかの目立たない違いが存在します。

1つ目の違いは、インターフェースはどこにでも使用できる新しい名前を作り出すことです。 型のエイリアスはインスタンスのために新しい名前を作成せず、エラーメッセージにはそのエイリアス名は使用されません。

下記のコードで、エディターでinterfaced上をホバーするとInterfaceが返されることが表示されますが、 aliasedではオブジェクトのリテラル型が返されることが表示されます。

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

2つ目のより重要な違いは、型のエイリアスは拡張や実装が出来ないということです。 (他の型からのextend/implement) ソフトウェアの理想的な特徴に拡張が許されていることが挙げられることから、 あなたは可能な限り、型のエイリアスよりもインターフェースを使用するべきでしょう。

一方で、もしインターフェースを形作る式を書くことができず、尚且つ共用体(union)、またはタプル型の使用が必要とされるケースであれば、 型のエイリアスを使用するべきでしょう。

文字列リテラル型(String Literal Types)

文字列リテラル型(String literal type)は、文字列が持たなければいけない厳密な値を強制させることを可能にします。 実際、文字列リテラル型は、共用体型(union types)、型の保護、型のエイリアスと申し分なく組み合わさります。 これらの機能を一緒に使用することで、文字列を使用したenumのような挙動を利用することが可能になります。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // エラー: "uneasy"は許可されません

許可される3つの文字列のいずれかを渡すことが可能であり、それ以外の文字列を渡すとエラーになります。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

文字列リテラル型は、オーバーロードを区別するのと同じ方法で使用することができます。

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... その他のオーバーロード ...
function createElement(tagName: string): Element {
    // ... 処理コード ...
}

判別共用体(Discriminated Unions)

文字列リテラル型、共用体型(union types)、型の保護、そして型のエイリアスを組みあせて、 「判別共用体(discriminated unions)」と呼ばれる高度なパターンを構築することが可能で、 また「タグ付けされたunion(tagged unions)」、「代数的データ型(algebraic data types)」としても知られています。

判別共用体は、関数型言語で便利です。 ある言語では自動的に共用体(union)を識別します。 TypeScriptはそのようにはせず、既に存在するJavaScriptのパターンを構築します。(翻訳に自信なし) これには4つ(訳注: 3つの間違い?)の要素が存在します。

  1. 共通する型、文字列リテラルのプロパティ - 判別
  2. それらの型の共用体(union)を受け止める型のエイリアス - 共用体(union)
  3. 共通するプロパティの型の保護
interface Square {    //正方形
    kind: "square";
    size: number; //サイズ
}
interface Rectangle { //長方形
    kind: "rectangle";
    width: number;  //幅
    height: number; //高さ
}
interface Circle {    //円
    kind: "circle";
    radius: number; //半径
}

まず、共用体(union)にするインターフェースを宣言します。 各インターフェースは、異なる文字列リテラル型のkindプロパティを持ちます。

kindプロパティは「判別(discriminant)」または「タグ(tag)」と呼ばれます。 他のプロパティは、各インターフェース特有のものです。 このインターフェースには、目下のところ関連性が無いことに注意してください。 これらを共用体(union)として配置してみましょう。

type Shape = Square | Rectangle | Circle;

それでは、判別共用体を使用してみましょう。

function area(s: Shape) { //面積
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

チェックの徹底化

我々は、判別共用体の全ての種類がカバーできていない場合には、コンパイラにそのことを伝えて欲しいと考えるでしょう。 例えば、もしTriangleShapeに加えた場合、areaを同様に更新する必要があります。

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    //ここでエラーになるべき: "triangle"の扱いが無いため
}

これを行うためには、2つの方法が存在します。 1つ目は、--strictNullChecksを有効にし、戻り値の型を指定することです。

function area(s: Shape): number { // エラー: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

switchに漏れができてしまったことから、 TypeScriptは関数がundefinedを返す可能性があることに気づきます。 明確に戻り値の型がnumberであると指定しているので、 実際には戻り値の型がnumber | undefinedであるとエラーが発せられます。

ただし、この方法は非常に気付きにくいものであり、 また--strictNullChecksは古いコードで常に動作するとは限りません。

2つ目の方法はnever型を使用することで、コンパイラに徹底的なチェックを促すことです。

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

assertNeversneverの型であるかチェックするという趣旨ですが、 最終的にどんな場合でも排除されるようになっています。 caseの指定忘れがあると、sは実際の型を持つことになり型エラーが発生することになります。 この方法は余分な関数の定義が必須となりますが、case忘れの対処としては最適なものでしょう。

多様性のthisの型(Polymorphic this types)

多様性のthisの型は、クラスまたはインターフェースを含む部分型を表します。 これは、F-bounded polymorphismと呼ばれ、式を階層的で流動性のあるものにしてくれます。 下記は、各演算の後にthisを返すシンプルな計算器の例になります。

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... その他の演算処理 ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

このクラスはthisの型を使用するため、これを拡張して新しいクラスで元のメソッドを変更なしに使用することが可能です。

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... その他の演算処理 ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

thisの型が無ければ、 ScientificCalculatorBasicCalculatorを拡張して流動的な式を維持し続けることができません。 multiplyは、sinメソッドを持たないBasicCalculatorを返すでしょう。 ただし、thisの型があれば、multiplyScientificCalculatorであるthisを返してくれます。

 Back to top

© https://github.com/Microsoft/TypeScript-Handbook

このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • ドキュメントの情報が古い可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。