TypeScript の基礎知識
●Union Types
- TypeScript はクラスやインターフェースで新しいデータ型を定義できるが、それ以外の方法もある
- Union Types は "A または B" というデータ型を表す
- C言語の「共用体 (union)」や関数型言語の「代数的データ型」と似ている
- Union Types は複数のデータ型を '|' で連結して表す
データ型1 | データ型2 | ... | データ型n
共用型の場合、各データ型で共通のフィールドやメソッドしか使用できない
データ型固有のフィールドやメソッドを使用する場合は「型変換 (キャスト)」が必要 (あとで説明する)
let a: number | string; は数値または文字列を代入できるが、それ以外のデータ型を代入することはできない
オプション --strictNullChecks を指定すると、変数 a に null や undefined を代入することはできない
データ型を number | string | null にすると、null を代入することが可能になる
TypeScript は type でデータ型に別名を付けることができる
type 名前 = データ型;
これを Type Alias という
Type Alias は再帰的なデータ型でも定義できる
リスト : Type Alias の使用例 (sample16.ts)
// 以下のプログラムは --strictNullChecks を指定するとコンパイルエラーになる
// 簡単な連結リスト
type LinkedList<T> = { item: T; next: LinkedList<T> };
const ls0: LinkedList<number> = { item: 0, next: null };
const ls1 = { item: 1, next: ls0 };
const ls2 = { item: 2, next: ls1 };
console.log(ls2.item);
console.log(ls2.next.item);
console.log(ls2.next.next.item);
console.log(ls2.next.next.next);
console.log(ls2);
$ tsc sample16.ts
$ node sample16.js
2
1
0
null
{ item: 2, next: { item: 1, next: { item: 0, next: null } } }
リスト : Union Types の使用例 (sample17.ts)
// 以下のプログラムは --strictNullChecks を指定してもコンパイルできる
// 空リスト (本来ならばシングルトンにする)
class Nil {
get first(): never { throw new Error("empty list"); };
get rest(): never { throw new Error("empty list"); }
}
// コンスセル
class Cons<T> {
constructor(private _first: T, private _rest: List<T>) {}
get first(): T { return this._first; }
get rest(): List<T> { return this._rest; }
}
// 連結リスト
type List<T> = Nil | Cons<T>;
// 終端
const nil = new Nil();
// 生成関数
function cons<T>(x: T, xs: List<T>): List<T> {
return new Cons<T>(x, xs);
}
const xs0 = cons<number>(0, nil);
const xs1 = cons(1, xs0);
const xs2 = cons(2, xs1);
console.log(xs2.first);
console.log(xs2.rest.first);
console.log(xs2.rest.rest.first);
console.log(xs2);
$ tsc sample17.ts
$ node sample17.js
2
1
0
Cons {
_first: 2,
_rest: Cons { _first: 1, _rest: Cons { _first: 0, _rest: Nil {} } } }
- never はありえないことを表すデータ型である
- Nil の first(), rest() は throw でエラーを送出するので、呼び出し元には戻らない
- これを never 型でコンパイラに教えることにより、first(), rest() の返り値のデータ型がコンスセルのものに推論できる
- 実際には以下に示すように抽象クラス List<T> を定義したほうがよいかもしれない
リスト : 抽象クラスを使った連結リストの実装 (sample18.ts)
abstract class List<T> {
abstract get first(): T;
abstract get rest(): List<T>;
}
// 空リスト (本来ならばシングルトンにする)
class Nil extends List<never> {
get first(): never { throw new Error("empty list"); };
get rest(): never { throw new Error("empty list"); }
}
// コンスセル
class Cons<T> extends List<T> {
constructor(private _first: T, private _rest: List<T>) { super(); }
get first(): T { return this._first; }
get rest(): List<T> { return this._rest; }
}
// 終端
const nil = new Nil();
// 生成関数
function cons<T>(x: T, xs: List<T>): List<T> {
return new Cons<T>(x, xs);
}
const xs0 = cons<number>(0, nil);
const xs1 = cons(1, xs0);
const xs2 = cons(2, xs1);
console.log(xs2.first);
console.log(xs2.rest.first);
console.log(xs2.rest.rest.first);
console.log(xs2);
$ tsc sample18.ts
$ node sample18.js
2
1
0
Cons {
_first: 2,
_rest: Cons { _first: 1, _rest: Cons { _first: 0, _rest: Nil {} } }
}
- クラス Nil はクラス List<T> を継承するが、型パラメータ T の指定で困る
- この場合、never を指定するとうまくいく
- TypeScript の never はすべての型のサブタイプとして定義されている
- never は Scala の Nothing と似ている
リスト : never 型 (sample19.ts)
class Foo { foo: 1; }
class Bar extends Foo { bar: 2 }
class Baz extends Bar { baz: 3 }
function foo<T extends Bar>(): void { console.log("oops"); }
// foo<Foo>(); コンパイルエラー
foo<Bar>();
foo<Baz>();
// foo<number>(); コンパイルエラー
foo<any>();
foo<never>();
$ tsc sample19.ts
$ node sample19.js
oops
oops
oops
oops
- 関数 foo() の型 T は Bar のサブタイプなので、Bar, Baz は指定できても Foo や number は指定できない
- never はすべての型のサブタイプなので、never を指定してもコンパイルできる
- any を指定してもコンパイルできるとは驚いた
- T extends never とすると、never 以外の型はコンパイルエラーになる
- M.Hiroi は TypeScript の型システムに詳しくないので、何か間違いや勘違いがあるかもしれない
●Type Assertions
- type assertions (型アサーション) は他のプログラミング言語でいう「キャスト」のこと
- type assertions の構文は 2 種類ある
<データ型名> 値
値 as データ型名
いわゆる「ダウンキャスト」ができるが、変換できない場合は実行時に不具合が発生する
type assertions よりも、できれば Type Guards を使ったほうが良い
リスト : Type Assertions の使用例 (sample20.ts)
class FooX {
constructor(private _x: number) { }
get x(): number { return this._x; }
}
class BarX extends FooX {
constructor(x: number, private _y: number) {
super(x);
}
get y(): number { return this._y; }
}
class BazX extends FooX {
constructor(x: number, private _z: string) {
super(x);
}
get z(): string { return this._z; }
}
// アップキャスト
const objFoo0: FooX = new BarX(123, 456);
const objFoo1: FooX = new BazX(789, "hello, world");
// console.log(objFoo0.y); コンパイルエラー
// console.log(objFoo1.z); コンパイルエラー
// ダウンキャスト (正常に実行)
console.log((<BarX>objFoo0).y); // 456
console.log((<BazX>objFoo1).z); // hello, world
// コンパイルできるが実行結果は undefined
// objFoo0 は BazX のオブジェクトではないから
console.log((<BazX>objFoo0).z); // undefined
$ tsc sample20.ts
$ node sample20.js
456
hello, world
undefined
●Type Guards
- Type Guards (型ガード) は if 文でデータ型を比較することにより、オブジェクトのデータ型を特定する機能である
- データ型の比較には演算子 typeof と instanceof を使う
- typeof obj はオブジェクト obj のデータ型により次の値 (文字列) を返す
- number => 'number'
- string => 'string'
- boolean => 'boolean'
- undefined => 'undefined'
- 関数 => 'function'
- その他 (null も含む) => 'object'
- if (typeof obj === 'number') { ... この中で obj は数値として扱われる ... }
- obj instanceof データ型 は obj がデータ型のオブジェクトであれば真を返す
- if (obj instanceof A) { ... この中で obj は A として扱われる ... }
- ユーザがデータ型を判定するための関数を定義することができる
function 関数名(仮引数: データ型): 仮引数 is データ型 { ... }
返り値のデータ型を指定するとき is を使用する
実際の返り値は boolean
リスト : Type Guards の使用例 (sample21.ts)
function isFoo(x: {foo: string}): x is {foo: string} {
if (!x) return false;
return typeof x.foo === 'string';
}
function typeSample(obj: number | string | Array<number> | {foo: string} | undefined): void {
if (typeof obj === 'number') {
// obj は数値
console.log(obj + 123);
} else if (typeof obj === 'string') {
// obj は文字列
console.log("hello, " + obj);
} else if (obj instanceof Array) {
// obj は配列
console.log(obj.reduce((a, x) => a + x));
} else if (isFoo(obj)) {
// obj は {foo: string} と互換性あり
console.log(`foo: ${obj.foo}`);
} else {
// 残りは undefined
console.log("oops!!");
}
}
typeSample(1000);
typeSample("bar");
typeSample([1,2,3,4,5]);
typeSample({foo: 'oops!!'});
typeSample(undefined);
$ tsc sample21.ts
$ node sample21.js
1123
hello, bar
15
foo: oops!!
oops!!
●Intersection Types
- Intersection Types (交差型) は複数の型を & で連結したもの
- T & U & V は T, U, V のフィールドやメソッドをすべて備えた型になる
- データ型の合成を簡単に記述できる
リスト : Intersection Types の使用例 (sample22.ts)
type FooI = { foo: number };
type BarI = { bar: string };
type BazI = FooI & BarI;
const objX: FooI = { foo: 123 };
const objY: BarI = { bar: "hello, world" };
let objZ: BazI;
// objZ = objX; コンパイルエラー
// objZ = objY; コンパイルエラー
objZ = { foo: 456, bar: "oops!!" }; // OK
console.log(objX);
console.log(objY);
console.log(objZ);
$ tsc sample22.ts
$ node sample22.js
{ foo: 123 }
{ bar: 'hello, world' }
{ foo: 456, bar: 'oops!!' }
●String Literal Types
- 「String Literal Type (文字列リテラル型)」は文字列リテラルをデータ型として扱う機能
type Fruit = "Apple" | "Grape" | "Orange";
"Apple", "Grape", "Orange" はデータ型を表す
let fruit: Fruit と宣言した変数 fruit には "Apple", "Grape", "Orange" を代入することができる
それ以外の文字列は代入することができない
リスト : String Literal Types の使用例 (sample23.ts)
// 果物
type Fruit = "Apple" | "Grape" | "Orange" | "Banana";
// 果物の価格
class FruitPrice {
constructor(private _kind: Fruit, private _price: number) { }
get kind(): Fruit { return this._kind; }
get price(): number { return this._price; }
}
// 価格表
const priceTable: FruitPrice[] = [
new FruitPrice("Apple", 100),
new FruitPrice("Grape", 150),
new FruitPrice("Orange", 80)
];
// 価格を求める
function getPrice(x: Fruit, xs: FruitPrice[]): number {
for (let fruit of xs) {
if (fruit.kind == x) return fruit.price;
}
return 0;
}
console.log(getPrice("Apple", priceTable));
console.log(getPrice("Grape", priceTable));
console.log(getPrice("Orange", priceTable));
console.log(getPrice("Banana", priceTable));
// 簡単な Option 型
type Option<T> = "None" | { some: T };
function isNone<T>(x: Option<T>): x is "None" { return typeof x === 'string'; }
function isSome<T>(x: Option<T>): x is {some: T} { return typeof x !== 'string'; }
// Option から値を求める
function getValue<T>(x: Option<T>, y?: T) {
if (isSome<T>(x)) {
return x.some;
} else if (y !== undefined) {
return y;
}
throw new Error("getValue: value is none");
}
// 探索
function findIf<T>(pred: (x: T) => boolean, xs: T[]): Option<T> {
for (let y of xs) {
if (pred(y)) return { some: y };
}
return "None";
}
let result = findIf(x => x.kind == 'Apple', priceTable);
if (isSome(result))
console.log(getValue(result).price);
else
console.log("sold out");
result = findIf(x => x.kind == 'Banana', priceTable);
if (isSome(result))
console.log(getValue(result).price);
else
console.log("sold out");
$ tsc sample23.ts
$ node sample23.js
100
150
80
0
100
sold out
●関数の多重定義
- TypeScript は同名の関数を複数定義することができる
- これを関数の「多重定義 (overload)」という
- ただし、引数のデータ型、個数、並び方などが異なる必要がある
- TypeScript の多重定義は他の言語のそれとちょっと異なる
- 同名の関数を複数宣言するだけで、実装は一つの関数で行う
- このとき、関数の仮引数や返り値のデータ型は、関数宣言と矛盾しないように定義すること
リスト : 関数の多重定義 (sample24.ts)
// 関数宣言
function foo(): void;
function foo(x: number): void;
function foo(x: string): void;
// 実装
function foo(x?: any): void {
if (x === undefined) {
console.log("hello, foo");
} else if (typeof x === 'number') {
while (x-- > 0) console.log("hello, foo!");
} else {
console.log(`hello, ${x}!!`);
}
}
foo();
foo(5);
foo("oops");
$ tsc sample24.ts
$ node sample24.js
hello, foo
hello, foo!
hello, foo!
hello, foo!
hello, foo!
hello, foo!
hello, oops!!
●モジュール
- JavaScript は ECMAScript 2015 (ES2015) から「モジュール (mmodule)」が導入された
- TypeScript は ES2015 だけではなく、CommonJS 形式や AMD 形式などのモジュールをサポートしている
- ここでは CommonJS 形式のモジュールの使い方を簡単に説明する
- 一般に、TypeScript (JavaScript) は一つのファイルで一つのモジュールを定義する
- モジュールファイルで変数、関数、クラスなどの定義に exprot を付けると、それらの名前が外部に公開される
- Node.js の module.exports = クラス; と同じことをしたい場合は export = クラス; とする
- モジュールを読み込むときには import 文と reqire() を使う
import 変数名 = require("...パス.../モジュール名");
export された名前は 変数名.名前 でアクセスすることができる
export = クラス; の場合は、クラスが変数に格納される (Node.js と同じ)
コンパイルするときはオプション -m commonjs を指定すること
リスト : モジュール foo.ts
class Foo {
static foo(): string { return "foo"; }
static bar(): string { return "bar"; }
static baz(): string { return "baz"; }
}
export = Foo;
リスト : モジュールの使用例 (sample25.ts)
import foo = require("./foo");
console.log(foo.foo());
console.log(foo.bar());
console.log(foo.baz());
$ tsc -t es2015 -m commonjs sample25.ts
$ node sample25.js
foo
bar
baz