高等な型
- 交差型(Intersection Types)
- 共用体型(Union Types)
- 型の保護と型の識別(Type Guards and Differentiating Types)
- Nullの型(Nullable types)
- 型のエイリアス(Type Aliases)
- 文字列リテラル型(String Literal Types)
- 判別共用体(Discriminated Unions)
- 多様性のthisの型(Polymorphic this types)
交差型(Intersection Types)
交差型(intersection type)は複数の型を1つに連結したものです。 これは既存の型を統合し、必要とする機能を全て備えた1つの型を得ることを可能にしてくれます。
例えば、Person & Serializable & Loggable
は、Person
とSerializable
とLoggable
です。
これは、この型のオブジェクトが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の両方で共通するメンバを持つことのみが確定されます。
この例では、Bird
はfly
というメンバを持ちますが、
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
部分の中でpet
がFish
であることを知るだけで無く、
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は下記の順で次のように処理します。
-
関数の
prototype
のプロパティがany
でなければ、その型に狭め、 - その型のコンストラクタ・シグネチャによって返される型の共用体(union)に狭めます。(翻訳に自信なし)
Nullの型(Nullable types)
TypeScriptには特別な型null
とundefined
が存在し、それぞれnullとundefinedの値を持ちます。
これらは、基本の型のセクションで概要を少しだけ説明しています。
デフォルトでは、型チェッカーはnull
とundefined
はどんなものにも割り当て可能であるとみなします。
実際に、null
とundefined
はあらゆる型で有効な値です。
つまり、例えそれを禁止にしたいとしても、どの型にも割り当てられないようにすることはできません。 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のセマンティクスと一致させるために、null
とundefined
を別々に扱うことに注意してください。
string | null
は、string | undefined
、string | 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)の演算子を使用して手動で削除することができます。
接頭辞の!
は、型からnull
とundefined
を取り除きます。
// 誤
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つの間違い?)の要素が存在します。
- 共通する型、文字列リテラルのプロパティ - 判別
- それらの型の共用体(union)を受け止める型のエイリアス - 共用体(union)
- 共通するプロパティの型の保護
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;
}
}
チェックの徹底化
我々は、判別共用体の全ての種類がカバーできていない場合には、コンパイラにそのことを伝えて欲しいと考えるでしょう。
例えば、もしTriangle
をShape
に加えた場合、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
}
}
assertNever
はs
がnever
の型であるかチェックするという趣旨ですが、
最終的にどんな場合でも排除されるようになっています。
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
の型が無ければ、
ScientificCalculator
はBasicCalculator
を拡張して流動的な式を維持し続けることができません。
multiply
は、sin
メソッドを持たないBasicCalculator
を返すでしょう。
ただし、this
の型があれば、multiply
はScientificCalculator
であるthis
を返してくれます。
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。