はじめに
いまさら JavaScript をやるのもアホらしいし,どうせなら TypeScript を武器にして他のフレームワークに挑戦したいと思う方こそ,まずは基礎基本を見直すことから始めるのが良い気がする.この記事では,TypeScript: Playground を利用して少しずつ TypeScript の書き方を学ぶ.JavaScriptを知っている前提の「差分」を通して学ぶのではなく,TypeScriptとしてイチからやっていく方針とする.
特に提示しないが,画面半分に解説記事,もう半分にPlayGroundといった配置にすることで,タブ切り替え等なく快適に学べる.できれば,ショートカットキーも一通りさらってみるのもいいかもしれない.
※なお,本記事は「TypeScriptで学ぶJavaScript入門 - @IT」 の内容に沿って進めている.途中省いた部分も多いので,より詳細な情報がほしい場合は,こちらを参照のこと.
TypeScriptの概要
terminal
や PowerShell
ではなく ブラウザ上で動きを確認する. この場合,他の言語の print
文に相当するものとして alert()
を用いる(console.log()
という手もあるが,それだと一手間増えるため不採用).
alert()
alert("Hello, TypeScript.");
alert
メソッドで引数にとった文字列(string)を,ブラウザのアラートボックスに表示する.
var Message:string;
Message = "Hello Typascript.";
alert(Message);
var
というキーワードで 変数宣言 を行ない,さらにコロン記号の後に続けて データ型 を指定する.Message
という変数が string
型であるから,"Hello Typascript."
と代入できる.最後に,Message
を引数にとって alert
させる.
クラス定義
// 変数宣言 + データ型指定
var Message: string;
// クラスオブジェクトの定義
class Cat {
age: number;
weight: number;
}
// クラスオブジェクトを変数`myCat`に代入
var myCat = new Cat();
myCat.age = 3;
myCat.weight = 5.1
Message = "My cat is " + myCat.age + " years old and its weight is " + myCat.weight + " kg.";
alert(Message);
class Hoge
に続けてブレース(波括弧)の中に 要素名(member):データ型(type)
のように定義できる.var foo = new Hoge()
のようにクラスオブジェクトとして初期化すれば,foo.要素名
としてそれぞれの要素にアクセスできる.すべて同じ string
型であるから,そのまま「+」で結合できる.
(注:プログラミングでは主として,カッコ,角カッコ,波カッコの三種類を使い分けるが,それぞれ英語では「パーレン」「ブラケット」「ブレイス」と呼ぶことが多いようだ → Bracket - Wikipedia)
TypeScriptの変数
JavaScriptやPythonは「動的型付け」であったが,TypeScriptは「静的型付け」な言語である.その特徴として,次のようなことが挙げられる.
- 変数を使う前には宣言が必要である
- 変数の宣言時には変数の名前を指定する必要がある
- 変数の宣言時には変数のデータ型が指定できる
必ず変数宣言をすること
これがTypeScriptの鉄則である.
// var price; // 何もデータ型をしていしないとAny型になる
// price = 1200;
// データ型を指定して変数宣言し,同時に値を代入
var price: number = 1200;
alert(price);
このように毎回データ型を指定することで,宣言されていない変数が使われてしまうという問題をあらかじめ排除しておける.
データ型の種類
データ型には,以下のようなものがある.
- Boolean
- Number
- String
- Array
- Tuple
- Enum
- Any
- Void
- Null and Undefined
- Never
- Object
詳細は,TypeScript: Handbook - Basic Types をみるとよい.
リテラル
決まった値を表すもの.そのリテラルに属するものしか表せないもの.整数リテラルなら整数だけ,文字列リテラルなら文字列だけ.
深い話になるのでここでは割愛.TypeScript リテラル
で検索のこと.
式・演算・条件分岐・繰り返し
この辺はほとんど他の言語と同じ.言語によっては,case
switch
が無いなど違いもあるが,そのあたりは省略.
配列
var musicians: string[] = new Array(5) // (1)
// または
// var musicians: Array<string> = new Array(5);
musicians[0] = "taiko"
musicians[1] = "big taiko"
musicians[2] = "small taiko"
musicians[3] = "fue"
musicians[4] = "utai"
alert(musicians[2])
変数 musicians
に対して,string[]
または Array<string>
というデータ型を指定して変数宣言する.new Array(5)
では,new
が オブジェクトを作成するための演算子 として働き,Array()
で要素数が 5 である配列を作成している.
その後,一つ一つにインデックスを指定して要素を代入しているわけだが,これはわざわざ一つずつ処理せずとも,最初からまとめて宣言できる.
var musicians: string[] = [
"taiko",
"big taiko",
"small taiko",
"fue",
"utai"
]
alert(musicians[2])
謎の挙動に注意
var a: Array<number> = new Array(3)
a[0] = 0
a[1] = 10
// 3つ目の要素に,存在しないはずのインデックスを指定
a[10] = 100
// 存在しないはずの4つめの要素を代入
a[20] = 200
alert("2番は" + a[2] + "、10番は" + a[10] + "、配列のサイズは" + a.length)
a[2]
は undefined
だが,a.length
は「21」となってしまう.コレは, 最大インデックス+1
を返す仕様になっているから(現在,最大インデックスは a[20]
).
※実際に配列の要素数を取得したいときは,Object
のkeys
メソッドに配列名を指定し、プロパティの配列を取得してその大きさを求める.
連想配列
インデックスとして数値だけでなく文字列が使用できる配列 だと考えられる.使い方は次の通り.
- オブジェクトリテラルを使ってオブジェクトを作成し、プロパティを追加して連想配列のように取り扱う
- Arrayオブジェクトを作成して、配列のインデックスに文字列を指定する
オブジェクトリテラル
オブジェクトリテラルとは、オブジェクトに含まれる要素を{}で囲んで記したもの.
var Player = {
Pitcher: "岩田",
Catcher: "梅野"
}
alert(Player["Pitcher"])
留意する点としては,以下の通り.
- インデックスは文字列だが,引用符で囲んでも囲まなくてもよい
- ただし,引用符で囲む必要な場合もある
- インデックスが数字で始まる場合
- "1st", "2nd", etc.
- 途中にスペースを含む場合
- "short stop", etc.
- インデックスが数字で始まる場合
var Player = new Object()
と初期化しても同様にオブジェクトリテラルの連想配列として利用できる.
また連想配列では,ドット演算子をつかって要素にアクセスできる.ただし,変数宣言時に定義されたものに限る.
例えば,ドット演算子をつかってプロパティとして Pitcher
, Catcher
にアクセスしたいとき,はじめから var Player = { Pitcher: "岩田", Catcher: "梅野" }
としていれば問題ない.しかし,var Player = {}
とか,var Player: Object = { Pitcher: "岩田", Catcher: "梅野" }
とした場合には,初期化時のプロパティはそれぞれ 「{}
」 と「プロパティなし」になってしまう.(Object
型にはプロパティが存在しないため)
インデックスシグネチャ
インデックスや要素のデータ型を指定する仕組み.
こうすることで,Object
オブジェクト(オブジェクトリテラル)の内部で使われるインデックスや要素についてデータ型を指定することができる.
ここで問題となるのは,インデックスシグネチャを追加するとプログラムが見にくくなる ことである.そこで,interface
というキーワードを使って,特定のインデックスシグネチャに名前をつけておく.こうすれば,再利用しやすくなるし,なにより視認性が高まる.
interface Dictionary {
[index: string]: string
}
var Player: Dictionary = { Pitcher: "岩田", Catcher: "梅野" }
alert(Player.Pitcher)
関数
TypeScriptやJavaScriptならではの特徴として,関数もオブジェクトである というものがある.かんたんな関数から初めて,その特徴も徐々に見ていく.
書き方には3つの方法がある.
- 関数宣言
- 関数式
- アロー関数
関数宣言
function add2(x: number, y: number): number {
return x + y
}
var answer: number = add2(10, 20)
alert(answer)
キーワード function
に続けて関数名を書き,そのすぐ後ろのカッコ内に仮引数(呼び出されるまで値が決まらないから「仮」と呼ぶ)としてリストを渡す.カッコを抜けて直後には,関数の返り値のデータ型を指定する.
呼び出すときは関数名を書き,それに続けてカッコを作り,その中に実引数を与える.
一般に、英語では仮引数のことを「parameter」(パラメーター)と呼び、実引数のことを「argument」(アーギュメント)と呼んで区別している,とのこと
関数式
"関数もオブジェクトである" のであれば,変数に代入できるはず であるといえる.
var add2 = function(x: number, y: number): number {
return x + y
}
alert(add2(10, 20))
このとき,変数 add2
は function
型とも言われるデータ型になっているが,細かい話になるので割愛.
アロー関数(ラムダ式)
上述の2つの書き方を簡潔にして,(引数のリスト) : 戻り値の型 => { 関数の処理 }
のような形で式を書く.
var add2 = (x: number, y: number): number => {return x + y}
alert(add2(10, 20))
// 返り値が単純ならば,そのまま return なしで直接返り値を書いても良い
// var add2 = (x: number, y: number): number => x + y
複数の返り値が欲しいとき
記法としては,複数の返り値を並べて返すことはできない.
が,返り値としてオブジェクトを指定することで,複数の値をまとめて返せる.
function div2(x: number, y: number) {
var q: number = Math.floor(x/y)
var r: number = x - q * y
return {quotient: q, reminder: r}
}
var result = div2(20, 8)
alert("商は" + result.quotient + "、余りは" + result.reminder)
関数でも変数でも,データ型を指定していないのが気になるかもしれない.しかし,これを単に object
とだけ書いておくと,プロパティがないというエラーが出る.おとなしく型推論に任せるほうが良い.
ポイントとなるのは,返り値の指定方法だ.{ }
の中に「プロパティ名: 値」をカンマで区切っていくつか書くことになる.
関数に関連したいくつかのトピック
引数
オプション引数
function calcCost(price: number, amount: number, discount?: number): number {
if (discount) {
return price * amount * (1 - discount)
} else {
return price * amount
}
}
alert(calcCost(1200, 10))
alert(calcCost(1200, 10, 0.1))
関数 calcCost
の仮引数である discount
は ?
を末尾につけることで オプション引数 となる.すなわち,関数を呼び出す際に,引数に値を与えても与えなくても良いということだ.(あってもなくても機能する)
可変長引数
// 返り値が複雑なので,自分から指定せず型推論に任せる
function paramtest(arg1: number, ...restparam: number[]) {
return "first item of restparam: " + restparam[0] + "\n" +
"length of restparam: " + restparam.length
}
alert(paramtest(1,2,3,4,5))
restparam
が可変長引数となっている。可変個のデータを受け取る仮引数はこのように「...」に続けてその名前を記述する。この仮引数の型は配列型でなければならず、特に指定をしない場合にはany[]型となる(上ではnumber[]型となるように指定)。この場合、関数呼び出し時に省略不可能な引数に割り当てられたもの以外の実引数が配列としてrestparamに割り当てられる。例えば、「paramtest(1, 2, 3, 4, 5)」という呼び出しでは、「1」が仮引数arg1に、残りの2~5が配列としてrestparamに割り当てられる
引数の既定値
省略可能な引数には既定値を設定できる.データ型指定の後に,そのまま代入したい値を書けば良い.
function paramtest(arg1: number, arg2: number = 2, ...restparam: number[]) {
return "arg1: " + arg1 +
"\narg2: " + arg2 +
"\nfirst item of restparam: " + restparam[0] +
"\nlength of restparam: " + restparam.length;
}
// undefined を渡しても,arg2には既定値が設定されているので無問題
alert(paramtest(1, undefined, 3, 4, 5))
オーバーロード
同じ名前を持ち、異なる引数リストや戻り値の型を持つ複数の関数を定義すること
引数のデータ型によって呼び出される先が異なる同名関数を作ることができるが,それぞれ個別に定義するのではなく,それぞれのデータ型で宣言してから,引数に any
を用いた関数を一つだけ定義する. (引数のデータ型ごとの条件分岐をその中で行なう)
function getLength(x: number): number;
function getLength(x: string): number;
function getLength(x: any): number {
if (typeof (x) == "string") {
return x.length
} else {
if (x == 0) return 1;
return Math.floor(Math.log(x) / Math.LN10) + 1;
}
}
alert(getLength(123));
ジェネリクス
データ型を仮に決めておき,実際に使用するデータ型を呼び出し時に変えられるようにする機能. 総称型とも呼ばれる.
通常は,関数を定義する際にデータ型を指定するが,ジェネリクスではこの際に「仮のデータ型」を指定する.そして,後から呼び出す際に実際に必要なデータ型を指定する.つまり,データ型指定がいつでも柔軟に変えられる関数である.
function parrot<T>(data: T): T {
var ret: T
ret = data
return ret
}
alert(parrot<number>(100))
alert(parrot<string>("abc"))
呼び出すときのデータ型指定は,関数名の直後に <>
を付けてその中にデータ型を入れる.もちろん,定義の際に複数の仮データ型指定を行えば,呼び出す際に複数のデータ型を指定することもできる.
ここで一つ問題が発生する.関数内ではデータ型が仮定のものであるため,そのプロパティにアクセスしようとすると それが利用可能かわからない ことになる.小細工すれば解決できないこともないが,結論としては次のようになる.
ジェネリックスは、さまざまなデータ型の引数に対してきめ細かく処理を分けるためではなく、どのデータ型に対しても同じような処理をしたい場合に使う とよい(きめ細かく処理を分けたいのであれば、ジェネリックスを使うのではなく、関数をオーバーロードすればいい)
クロージャー
関数が定義された環境にある変数を利用できる機能
関数の中に入れ子関数を定義するときに,親関数の変数を利用できるということらしい(個人的見解).
function getSerialNumber() {
var origin = 0
function countUp(delta: number): number {
// 親関数 getSerialNumber のローカル変数 origin を流用
return origin += delta
}
return countUp
}
// inside の範疇では,origin = 0 として初期化される
// 関数 getSerialNumber が返すのは,子関数 countUp だけである
var inside = getSerialNumber()
// getSerialNumber ではなく countUp だけを呼び出しているので
// origin は初期化されず,保持されたまま
alert(inside(2)) // origin += 2
alert(inside(3)) // origin += 3
ここで,countUp
という関数名は必ずしも必要でないことが分かるすなわち,countUp
関数を無名関数にしてもいい.さらに,いちいち変数を inside
などと定義しなくてもよい.すべて完結に書き直すと以下のようになる.
var countUp = function () {
var origin = 0
return function (delta: number) {
return origin += delta
}
} () // 関数定義のあとにこのカッコをつけると即時実行関数になる(定義と実行が同じタイミングになる)
// 即時実行されているため,変数 origin は初期化されている
alert(countUp(2))
alert(countUp(3))
クラス
オブジェクト指向の復習
例えば,ペットの健康管理のプログラムを作るために「猫」という動物を表したい状況を考える.
身長と体重を単に変数として定義するのではなく,「これは 猫の 身長と体重である」とまとめて表したい.
ある目的に従っていくつかの変数や関数をまとめたものを、単なる変数と区別してオブジェクトと呼ぶ。
TypeScriptでは、より厳密、かつ柔軟にオブジェクトが取り扱えるようになっている。そのために使われるのがクラスである。
クラスとは、個々のオブジェクトではなく、オブジェクトのひな型とでもいうべきものである。
クラスを用いて「一般的に猫全般に言える特徴」を記述し,個々の猫については,そのクラスを引き継いで定義させる.このときの各猫オブジェクトを インスタンス(実体) と呼び,単なるオブジェクトや変数とは区別する.
class Cat {
length: number;
weight: number;
}
要するに、これまでと同じように変数を一通り宣言し(ただし、varは不要)、クラス名を使って一括りにしてやればよい。
また他に区別する事柄としては,以下の3つがある.
- クラス中に含まれる変数は,プロパティと呼ぶ
- クラス中に含まれる関数は,メソッドと呼ぶ
- これらを合わせて,クラスのメンバー(要素)と呼ぶ
クラスの内側か外側で呼び名が変わるだけで,定義の方法などは同じである.
インスタンスの作成
クラスは単なる雛形である.すなわち,そのままでは実際のデータを扱えない.データの実体をもいえるインスタンスを作成することで初めて個々のデータに触れられる.
個々の猫を表すには,猫クラスを引き継いで猫Aインスタンスを作る必要があるということである.そして,** インスタンスの作成には new
演算子を用いる.** newの後に「クラス名()」と書けば新しいインスタンスが作成できる.
「クラス名()」としたことで,インスタンスの参照(ポインタ?)が返される.これを変数 myCat
に代入すれば,いつでも myCat
からインスタンスを利用できる.
クラスのメンバーの利用
「.」で区切ってメンバーを書けば、クラスのメンバーが利用できる。
インスタンス myCat
は,猫クラスにおいて身長 length
, 体重 weight
という変数すなわち プロパティ が設定されていれば,myCat.length
myCat.weight
といった形でそれらを参照できる.
class Cat {
// コンストラクタ(後述)で初期化しないのでエラー
length: number;
weight: number;
// 末尾に ! で回避可能
// length!: number;
// weight!: number;
}
var myCat = new Cat()
myCat.length = 30.5
myCat.weight = 2.5
alert("体長は" + myCat.length + "cm、体重は" + myCat.weight + "kgです")
クラスのメンバーとしてメソッドを定義する
クラス内に関数を定義すると,メソッドと呼ばれる(通常の関数とは区別している).
class Cat {
length: number;
weight: number;
meow(): string {
return "にゃーん"
}
// 自身のプロパティを参照したいときは
// this.PROPERTY とする
eat() {
this.length += 0.1
this.weight += 0.1
// length += 0.1 だとエラー
}
}
var myCat = new Cat()
myCat.length = 30.5
myCat.weight = 2.5
myCat.eat(); // (3)
alert("私の猫は" + myCat.meow() + "と鳴き\n" +
"体長は" + myCat.length + "cm" +
"体重は" + myCat.weight + "kgです")
メソッドをオーバーロードする際も,基本と同じである.同じ名前で異なる引数を持つメソッドを宣言し、いずれの場合にも対応できるように any
型で一つメソッドを作り,その中で引数のデータ型によって条件分岐させればよい.
class Cat {
length: number
weight: number
meow(): string // 引数なしの場合
meow(s: string): string // 引数ありの場合
meow(s?: any): string{ // あってもなくてもいいから「?」をつける(オプション引数)
if (typeof (s) == "string") {
return s
} else {
return "にゃーん"
}
}
eat() {
this.length += 0.1
this.weight += 0.1
}
}
var myCat = new Cat()
myCat.length = 30.5
myCat.weight = 2.5
myCat.eat(); // (3)
alert("私の猫は" + myCat.meow("みゃお") + "と鳴き、\n" +
"体長は" + myCat.length + "cm、" +
"体重は" + myCat.weight + "kgです")
コンストラクタ
インスタンスの作成時に自動的に実行されるメソッドで、初期値の設定などに使われる。
初期化時にちくいちプロパティを設定しなくても良くなる.特殊なメソッドとして実装されており,constructor()
に続けて自動的に実行したい処理を書く.
class Cat {
length: number
weight: number
name: string
// コンストラクタを設定することで,これまでプロパティ側で発生していた
// `no initializer ...` というエラーが消えるはず
constructor() {
// 各々コメントアウトしてエラーを確認
this.length = 30.5
this.weight = 2.5
this.name = "名なし"
}
}
var myCat = new Cat()
alert("名前は" + myCat.name + "です")
コンストラクタもメソッドの一つであるから,前節と同様に,オーバーロードすることができる.
情報の隠蔽
今まで通りにクラスを定義すると,プロパティに対して自由度が高すぎる.そこで,ある程度の制限をかける(プロパティの操作に手続きを設ける)ことを考える.これを実現するためには,次の2つを実行すれば良い.
- クラスの外から直接プロパティを変えられないように参照を禁止する
- メソッドを経由して初めてプロパティを参照できるようにする
通常,プロパティは public
(クラス外から参照可能)となっているが,プロパティ名の直前に private
演算子を置くことでクラス外から参照を禁止できる.
プロパティを参照できる setName
メソッドに猫の名前「タマ」を渡すことで,猫クラスのプロパティ name
を間接的に変更する.
また,猫クラスの name
プロパティを参照して名前を知りたいときは,getName
メソッドを通じて間接的に参照する.
※当然のことながら,これらのメソッドは自分で定義する必要がある.
class Cat {
private name: string
public setName(s: string) {
this.name = s.slice(0, 8)
}
public getName(): string {
return this.name
}
// constructor は private にすると
// 初期化されても参照できなくなる
constructor () {
this.name = "名無し"
}
}
var myCat = new Cat()
myCat.setName("じゅげむじゅげむごこうのすりきれねこ")
alert("私の猫の名前は" + myCat.getName() + "です")
このように、重要な情報をプライベートな変数にして、クラスの外から勝手に変更されないようにすることを「情報の隠蔽」と呼ぶ。
変数の宣言やメソッドの定義の前に何も書かなかった場合には、publicが指定されたものと見なされる
クラスの継承
元のクラス(親クラス)の機能を全て受け継いだ新しいクラス(子クラス)を定義すること
新たなクラスを定義するときに,いちいち最初から一つずつ定義するのは骨が折れる.似通った部分が多いのなら,それを流用する方が都合がいい.そのための機能がクラス継承である.
人によって呼び方が異なる場合がある.以下の呼び方はすべて同様の意味である.
- 親クラス と 子クラス
- 基底クラス と 派生クラス
- スーパークラス と サブクラス
class Cat {
private name: string
public setName(s: string) {
this.name = s.slice(0, 8)
}
public getName(): string {
return this.name
}
public meow(): string {
return "にゃーん"
}
constructor () {
this.name = "名無し"
}
}
// 「子クラス名 extends 親クラス」という書き方
class Tiger extends Cat {
// 子クラス独自のメソッドも定義できるし,
// 親クラスのメソッドを上書き(オーバーライド)することもできる
public meow(): string {
return "がおー"
}
// 親のメソッドをそのまま流用したいときは,
// 直前に superをつけてやればいい
public meowlikecat(): string {
return super.meow();
}
}
var myTiger = new Tiger()
myTiger.setName("とらお")
// Catクラスを継承しているので,myTigerでは定義していないはずの
// getName() メソッドが使える
alert("私の虎の名前は" + myTiger.getName() + "で、" +
myTiger.meow() + "と鳴きます\n" + "が、甘えているときには" +
myTiger.meowlikecat() + "と鳴きます")
- 子クラス独自のメソッドも定義できるし,クラスのメソッドを上書き(オーバーライド)することもできる
- 親のメソッドをそのまま流用したいときは,直前に superをつけてやればいい
- Catクラスを継承しているので,myTigerでは定義していないはずのgetName() メソッドが使える
おわりに
TypeScriptで学ぶJavaScript入門 - @IT を題材として TypeScript のあれこれを学んだ.フレームワークをつまみ食いすることから始める人も多いが,クラスやプロパティやら継承やら,基礎基本がわからないうちに仕組みを理解するのはかなり難しい(実際に私は Vue
に入門して挫折してから再度基本をやり直す目的でこれを見直している).
どれだけ複雑に見えても,それは基本の組み合わせでしかないはず.ひとつひとつ着実に理解して,意識せずとも使いこなせるようにしたい.