TypeScript
設定環境
JavaScript是弱型別語言,TypeScript原則上就是JavaScript,但是是強型別。TypeScript的程式需經過編譯(compile)然後產生JavaScript程式碼,有了JavaScript程式碼即可使用於網頁設計,其好處是可無視瀏覽器型號版本皆可使用。- 使用NPM(需先安裝Node.js)安裝,$ npm install -g typescript
- 若已安裝Visual Studio(2015up),應已內建。
const message: string = "Hello World!"; console.log(message);存檔為ts1.ts,然後在cmd輸入tsc ts1.ts(亦即要先compile,然後會產生ts1.js檔案),然後在html檔內的body tag內加上
<script src="ts1.js"></script>按下F12應可看到顯示,如此便完成了。
第一話、變數型態(Types)
TypeScript須符合JavaScript的命名規則,命名規則與其他語言如Java無太大差別(TypsScript可使用錢號($)開頭)。變數的影響範圍是函數範圍(Variables are functionally scoped.)。TypeScript會自動偵測變數型態以免意外指派給不適當的值。例如: 儘管如此,有時候還是無法偵測出變數型態,所以我們可以自己給定變數型態(type annotation)來避免混淆。對於變數而言,指派變數型態的方式是在變數名稱之後加上冒號(:)然後給型態,例如:var n:number = 10; var s = "Hello, World!" console.log(typeof(n) + " " +typeof(s));基本變數型態可能有number, string, boolean 也可以宣告變數為陣列:
var alphabets: string[] = ['a','b','c','d','e']; alert(alphabets);也可以宣告函數的輸入輸出型態:
var Hi:(name: string)=>string = function(name){ return `Hello ${name}`;//使用backquote建立多行字串,${}用來顯示變數,類似Python的f"" } console.log(Hi("Jenny"));物件的型態宣告:
var Card:{suit:string, rank:number} = { suit: "Dimond", rank: 1 } console.log(Card);原則上與JavaScript類似,只是在變數名稱後加上type annotation。此外,也可以使用interface來簡化type annotation:
interface Card{ suit: string; rank: number; } var acard: Card = { suit: "dimond", rank: 1 } console.log(`${acard.suit}${acard.rank}`);TypeScript還包含四種無屬性變數:
- underfined: 尚未給值的變數
- null: can be an intentional absence of an object value
- void: 無傳回值的函數
- never: The never type is used when you are sure that something is never going to occur.例如函數永不在結束點傳回或是函數永遠都會傳回例外。
function forever(): never { while(true) { console.log('print forever.'); } } forever();//最好別執行,最後要強制關掉
function throwError(message: string): never { throw new Error(message); } throwError("Error");
function printx(x:any){ console.log(x); } printx(10); printx("doremi");上例中的:any加不加結果都一樣,但是若是加上:number那麼非數字的輸入便會是錯的。
若是Array其內的元素為物件,那麼做法如下:
interface Card{ suit: string; rank: number; } enum Suit{ Spade=0, Heart, Dimond, Club } var pack: Card[] = []; // 僅須在此宣告陣列為某物件之陣列 for (let index=1; index<=13; index++) { for (let j=0; j<4; j++) { pack.push({suit: Suit[j], rank: index}); } } console.log(pack);TypeScript支持泛型,所以上例的陣列宣告可以修改如下:
var pack: Array<Card> = []; // 僅須在此宣告陣列為某物件之陣列-
第二話、Operators
原則上跟JavaScript相同,此處介紹一些須注意處。- ++, --: 用於any, number, enum。
enum suit{ Spade,Heart,Dimond,Club } var s = suit.Spade; console.log(suit[++s]);//先++。若是s++(先印s再++) >> 印出Spade, Dimond s++; console.log(suit[s]);
- +,-,*,/,%,<<,>>, >>>, & ^, |: Binary operators(+ is a special case)
var s:string = '1'; var n:number = +s; var nn = -s; console.log(typeof(n) + " " + typeof(nn)); // number number
- !, !!: !! converts a type to a Boolean.
var truth = 'a true thing'; var falsey : string; var invert = !truth; // false var truthTest = !!truth; // true var falseyTest = !!falsey; // false(empty string) var zero;// if zero=0 >> false, if zero=1 >> true var zeroTest=!!zero; console.log(invert + ', ' + truthTest + ', ' + falseyTest + ', ' + zeroTest);
undefined, null, false, '', 0, NaN都視為false。 - ?: if...else...的縮寫
var b = true; var message:string; if (b) { message = "yes"; }else{ message = "no"; } var newmessage = b? 'Yes': 'No'; console.log(`${message} and ${newmessage}`);
第三話、control flow
原則上與JavaScript相同,基本內容參考這裡。此處做點補充:var arr = ['a','b','c','d','e']; for (let i in arr) { // in 指index console.log(i + " " + arr[i]); } for (let i of arr) { // of 指值 console.log(i); }
第四話、函數
一樣可先參考JavaScript的函數。先來個例子:function biggest(a,b,c){ var big = a > b? a : b; return big > c? big:c; } console.log(biggest(10, 50, 30));因為TypeScript可以指定型態,所以可以修改如下:
function biggest(a:number,b:number,c:number):number{ var big = a > b? a : b; return big > c? big:c; } console.log(biggest(10, 50, 30));Anonymous Function:
let bigger = function(a:number, b:number):number { return a>b?a:b; } console.log(bigger(10,20));optional parameters: 可有可無的輸入參數。
function biggest(a:number, b:number, c?:number):number{ let big = a > b? a: b; if (c) { // check if c exists 等同於 (typeof(c) !== 'undefined') return big > c? big: c; } return big; } console.log(`${biggest(2,5,3)} and ${biggest(20, 18)}`);default parameters: 有初始值的參數。
function usdToNt(dollor:number, ratio:number = 29.595):number { return dollor*ratio; } console.log(usdToNt(10000) + "\t" + usdToNt(10000, 30));rest parameters: 不確定長度參數(型態為[]、僅能定義一個且須位於參數最後)。
function average(team:number=101, ...score:number[]):void{ let ave:number = 0; for (let s of score){ ave = ave + s; } ave = ave/score.length; console.log(`team ${team}'s average score is ${ave}`); } // Use undefined if the default value is used average(undefined, 80, 90, 98);此例中的第一個參數有default value,若是呼叫時不給定則rest parameter的第一個參數會被默認為team,解決方式是若要使用初始值,需要輸入undefined(這樣似乎還不如不要定義初始值)。
overloads: 定義多個相同名稱的函數(參數個數必須相同但輸出入型態不同)。
function addition(a:string, b:string):string; function addition(a:number, b:number):number; function addition(a:any, b:any):any { return a+b; } console.log(addition('a', 'b')); console.log(addition(3,9));arrow functions(lambda): 用於Anonymous Function。
var addition1 = (a:number, b:number) => a+b; var addition2 = (a:number, b:number) => {return a+b}; var addition3 = function (a:number, b:number) { return a+b; } console.log(`${addition1(9,9)}, ${addition2(1,8)}, ${addition3(2, 8)}`);以上寫法都可以,使用arrow則不需寫function關鍵字。
使用call, apply, bind...
JavaScript code:
function add(a, ...b){ let sum = a; for (z of b) { sum = sum + z; } return sum; } let s1 = add.call(this, 1,2); let s2 = add.apply(null, [3,4]); let s3 = add.bind(null, 10); let s4 = add.call(null, 10, 20); let s5 = add.apply(null, [20, 30]); console.log(s3(300, 500, 800)); console.log(s1 + " " + s2 + " " + s4 + " " + s5);TypeScript code:
function toadd(a:number, ...b:Array<number>):number{ let sum = a; for (let z of b) { sum = sum + z; } return sum; } let ta1 = toadd.call(this, 1,2); let ta2 = toadd.apply(null, [3,4]); let ta3 = toadd.bind(null, 10); let ta4 = toadd.call(null, 10, 20); let ta5 = toadd.apply(null, [20, 30]); console.log(ta3(300, 500, 800)); console.log(ta1 + " " + ta2 + " " + ta4 + " " + ta5);
第五話、interface
Interface as Type: 用作限定型態。interface node { x:number; y:number; } let nodeA: node = {x:1, y:2}; console.log(nodeA);Interface as Function Type: 用作限定函數型態。
interface node{ x:number; y:number; } interface arc{ (a:node,b:node):number; //method signature } function linelength(a:node, b:node):number { //兩點�"直線距離 return Math.sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); } function arclength(a:node, b:node):number { //兩點為直�'之半�"長度 let radius = linelength(a,b)/2; return Math.PI*radius*radius; } let nodeA:node={x:1,y:1}; let nodeB:node={x:10,y:5}; let anarc:arc = linelength; console.log(anarc(nodeA,nodeB)); anarc = arclength; console.log(anarc(nodeA, nodeB));Interface for Array Type: 定義Array的型態。
interface alist{ [index:number]:number; } let nArr: alist = [1,2,3]; console.log(nArr); interface blist{ [index:number]:string; } let mArr: blist=["Apple", "Zebra"]; console.log(mArr);使用generic(泛型)可能容易些:
let arr:Array<number> = [1,2,3]; let brr:string[] = ['apple','banana','pear']; console.log(arr+"\t"+brr);Optional Property: 可選擇的參數。
interface node{ id?:number; x:number; y:number; } let nodea:node = { x:1, y:2, } console.log(nodea);Read Only Property: 唯讀參數。
interface node{ readonly id?:number; x:number; y:number; } let nodea:node = { id:0, x:1, y:2, } // nodea.id = 10; >> cause error console.log(nodea);Interface Extends Interface: 根據原interface擴充新interface。
interface node{ readonly id?:number; x:number; y:number; } interface newnode extends node { disToOrigin:number; } let nodea:newnode = { x:1, y:2, disToOrigin:undefined } console.log(nodea);Implementing an Interface: 實作interface。
interface node{ readonly id?:number; x:number; y:number; } interface newnode extends node { disToOrigin:number; toOrigin: () => void; } class TheNode implements newnode{ id:number; x:number; y:number; disToOrigin:number; constructor(id:number, x:number, y:number){ this.id = id; this.x = x; this.y = y; this.toOrigin(); } toOrigin(){ this.disToOrigin = Math.sqrt(this.x*this.x+this.y*this.y); } } let nodea = new TheNode(0, 1, 1) console.log(nodea.disToOrigin);
第六話、classes
先參考JavaScript classes。class card{ suit: string; rank: number; constructor(suit:string, rank:number) { // optional this.suit = suit; this.rank = rank; } toString():string { return this.suit + " " + this.rank; } } let a1 = new card("Dimond", 1); alert(a1.toString());interface and inheritance: 介面與繼承(see also Java ch7)。
interface human { name: string; gender: string; } interface characters { strength: number; agileness: number; trainStrength: () => void; trainAgileness: () => void; } class man implements human, characters{ name: string; gender: string; strength: number; agileness: number; constructor(name: string, gender: string, strength: number, agileness: number) { this.name = name; this.gender = gender; this.strength = strength; this.agileness = agileness; } trainStrength() { this.strength++; } trainAgileness() { this.agileness++; } toString(): string { return `${this.name} is ${this.gender} and strength = ${this.strength}, agileness = ${this.agileness}`; } }//man interface hero { superpower: string[]; } class Spiderman extends man implements hero { superpower: string[]; constructor(name: string, gender: string, strength: number, agileness: number, superpower: string[]) { super(name, gender, strength, agileness); this.superpower = superpower; } printPower():void { this.superpower.forEach(element => { console.log(element); }); } } let peter = new Spiderman('Peter Parker', 'male', 9, 11, ['Genius-level intellect', 'Superhuman strength']); peter.trainAgileness(); console.log(peter.toString()); peter.printPower();data modifier: private, readonly。
class stock{ readonly item: string; private quantity: number; constructor(item: string, quantity: number) { this.item = item; this.quantity = quantity; } getQuantity() { return this.quantity; } setQuantity(q: number) { this.quantity = q; } toString():string { return `${this.item} has ${this.quantity} remained.`; } } let chip = new stock('chip', 100); chip.setQuantity(101); // chip.item = 'desk'; X >> item is readonly // chip.quantity = 101; X >> quantity is private console.log(chip.toString());ECMAScript 5 or above可以使用get與set關鍵字建立getter&setter。
可以使用Readonly
interface employee { id: number; name: string; } let emp1: Readonly<employee> = { id: 1, name: 'Tom' } // emp1.id = 2; X>> readonly type cannot be modified let emp2: employee = { id: 2, name: 'Jenny' } emp2.name = 'Jennifer'; console.log(emp2);static: static method (same as Java)。
class Mathematics { static pi = 3.14159; static square(w:number, h:number):number { return w*h; } static circle(radius:number): number { return this.pi*radius*radius; } } console.log(`${Mathematics.square(10,20)}, ${Mathematics.circle(10)}`);with cloure method: 可記錄是時變數值。
class ClickCounter { private count = 0; registerClick() { return () => { console.log(this.count++); }; } } var clickCounter = new ClickCounter(); // create a button with id=target in the html file document.getElementById('target').onclick = clickCounter.registerClick();可以改寫成如下:
class ClickCounter { private count = 0; registerClick() { this.count++; console.log(this.count); } } var clickCounter = new ClickCounter(); // document.getElementById('target').onclick = clickCounter.registerClick; << 寫這樣會出現NaN document.getElementById('target').onclick = function () { clickCounter.registerClick(); }或是使用bind:
class ClickCounter { private count = 0; registerClick() { this.count++; console.log(this.count); } } var clickCounter = new ClickCounter(); var clickHandler = clickCounter.registerClick.bind(clickCounter); document.getElementById('target').onclick = clickHandler;instanceof: 判斷是否是某物件之instance。
class Hero{ name: string; } class SpiderMan extends Hero {} class ShieldPersonnel {} let superman = new Hero(); let peter = new SpiderMan(); let coulson = new ShieldPersonnel(); console.log(`${superman instanceof Hero}`); // true console.log(`${peter instanceof Hero}`); // true console.log(`${coulson instanceof Hero}`); // false
第七話、Modules
雖然我們可以使用函數與物件來將程式分割為許多小區塊以方便管理,但是當程式碼漸大,還是難以維護,此時我們可以使用Modules,將程式碼分類於不同區塊或檔案內,可方便管理與維護。Modules可分為Internal與External兩類,Internal表示在同一檔案,External表示在不同檔案。Internal Modules:
module InternalModule { export var hi:string = "Hello"; } import mo = InternalModule; console.log(mo.hi);Modeules內的變數加上export關鍵字便可被import。也可以這樣寫:
module InternalModule.mo { export var hi:string = "Hello"; export class AClass{ fun() { return "Inside a function"; } } } import mo = InternalModule.mo; console.log(mo.hi); let ac = new mo.AClass(); console.log(ac.fun());module import an internal module:
module Vehicle { export interface Car { brand: string; wheels: number; cc: number; } export class Sedan implements Car { brand: string; wheels: number; cc: number; constructor (brand: string, wheels: number, cc: number){ this.brand = brand; this.wheels = wheels; this.cc = cc; } } }//module Vehicle module Vehicle.truck { import car = Vehicle.Car; export class Truck implements car{ brand: string; wheels: number; cc: number; constructor(brand: string, wheels: number, cc:number){ this.brand = brand; this.wheels = wheels; this.cc = cc; } } } import sedan = Vehicle.Sedan; var benz = new sedan('Mercedes-Benz', 4, 3000); console.log(benz); import truck = Vehicle.truck.Truck; let benzTruck = new truck('Mercedes-Benz', 12, 6000); console.log(benzTruck);External Modules: ts2.ts
export var hello:string = "Greeting."; export interface Cargo { id: number; content: string; value: number; } export class Commodity implements Cargo { id: number; content: string; value: number; constructor (id: number, content: string, value: number) { this.id = id; this.content = content; this.value = value; } }ts1.ts:
import * as Com from "./ts2"; let co = new Com.Commodity(1, "book", 10); console.log(co);
第八話、Generics
用來限定資料型態。Generic functions:
function reverse<T>(list: T[]) : T[] { let reversed: T[] = []; for (let i = (list.length-1); i>=0; i--) { reversed.push(list[i]); } return reversed; } let aList = ['a','b','c','d','e']; console.log(reverse(aList)); let nList = [1,2,3,4,5]; console.log(reverse(nList));Multiple Type Variables:
function twoArgs<T, U>(id:T, name:U): string { return id + "\t" + name; } console.log(twoArgs<number, string>(1, "Tom"));要注意的是在函數內僅能使用對每個type都適用的methods。(例如使用toUpperCase()僅適用於string便無法使用。) Generic Interfaces:
interface Pair<T, U> { one: T; two: U; } let p1: Pair<number, string> = {one:1, two:"Tom"}; let p2: Pair<string, boolean> = {one:"Full House", two: true}; console.log(p1); console.log(p2);Interface as Function Type:
interface Fun<T, U> { (one: T, two: U): void; } function makeFun<T,U>(one:T, two:U): void { console.log(`one = ${one}, two = ${two}`); } let fun1: Fun<string, number> = makeFun; let fun2: Fun<number, number> = makeFun; console.log(fun1('string', 1)); console.log(fun2(10, 20));也可以藉由實現interface來建立class,如下:
interface Fun<T, U> { haveFun(one: T, two: U): void; } class MakeFun implements Fun<string, number> { haveFun(one: string, two: number) { console.log(`one = ${one}, two = ${two}`); } } let fun1: Fun<string, number> = new MakeFun(); fun1.haveFun("string", 1);Generic classes:
class AClass<T, U>{ private one: T; private two: U; constructor(one:T, two:U) { this.one = one; this.two = two; } toString(): string { return `one = ${this.one}, two = ${this.two}`; } } let ac1 = new AClass<string, number>("string", 1); let ac2 = new AClass<number, number>(1, 2); console.log(`${ac1.toString()}\n${ac2.toString()}`);inherit generic class:
class Experience<T> { positions:Array<T>; constructor(positions: Array<T>) { this.positions = positions; } getPositions(): Array<T> { return this.positions; } } class CV extends Experience<string> { salary: number; constructor(public positions: Array<string>, salary: number) { super(positions); this.salary = salary; } toString(): string { return `${this.positions}, salary = ${this.salary}`; } } let cv = new CV(["Manager","CEO"], 180000); console.log(cv.toString());practice:先在.html檔案內加上
<ul> <li id = "s1"></li> <li id = "s2"></li> <li id = "s3"></li> </ul>接著完成TypeScript code:
class Song { name: string; url: string; randomNumber: number; constructor(name: string, url: string) { this.name = name; this.url = url; } } var songs = [ new Song("That's What Friends Are For","https://www.youtube.com/watch?v=HyTpu6BmE88"), new Song("Rhinestone Cowboy","https://www.youtube.com/watch?v=8kAU3B9Pi_U"), new Song("The Winner Takes It All","https://www.youtube.com/watch?v=j-YlndYkJl0"), new Song("I choose to love you","https://www.youtube.com/watch?v=XCobwOYE-iA"), new Song("Ma Baker","https://www.youtube.com/watch?v=oR6eKmqSEa0"), new Song("Rasputin","https://www.youtube.com/watch?v=kvDMlk3kSYg"), new Song("Whistle Down The Wind","https://www.youtube.com/watch?v=aZdyBHfL7ac"), new Song("誰可改變","https://www.youtube.com/watch?v=OJQTeBWVC8Y"), new Song("Whistle","https://www.youtube.com/watch?v=dISNgvVpWlo"), new Song("One more","https://www.youtube.com/watch?v=n9sEjiBew18"), new Song("I'm ill","https://www.youtube.com/watch?v=OtSS4xcdXpI"), new Song("MOYA","https://www.youtube.com/watch?v=oOEkySKLn-g"), new Song("香水有毒","https://www.youtube.com/watch?v=0EEIh5MucS8"), new Song("好想再聽一遍","https://www.youtube.com/watch?v=kNQFIKk4Fts"), new Song("愛你不是兩三天","https://www.youtube.com/watch?v=sPCKERuXiAo"), new Song("Roly-Poly","https://www.youtube.com/watch?v=3Xolk2cFzlo"), new Song("傷心留言","https://www.youtube.com/watch?v=DH9DAHt7598"), new Song("The way we were","https://www.youtube.com/watch?v=GNEcQS4tXgQ"), new Song("Love Love Love","https://www.youtube.com/watch?v=lfORLGHuB7o"), new Song("藍色生死戀","https://www.youtube.com/watch?v=jRCTAEAsUGQ&index=1&list=RDQMBpEGdG9X_48") ]; class MusicBox { songs: Array<Song>; constructor(songs: Array<Song>) { this.songs = songs; } compareTo(song1:Song, song2:Song):number { if (song1.randomNumber > song2.randomNumber) { return 1; }else if(song1.randomNumber < song2.randomNumber) { return -1; }else{ return 0; } } shuffle() { songs.forEach(element => { element.randomNumber = Math.random(); }); songs.sort(this.compareTo); } setSong() { this.shuffle(); let ids = ['s1', 's2', 's3']; for(let id in ids) { let aNode = document.createElement("a"); let hrefNode = document.createAttribute("href"); hrefNode.value = this.songs[id].url; aNode.setAttributeNode(hrefNode); aNode.setAttribute("target", "_blank"); let btEle = document.createElement("button"); btEle.setAttribute("class", "noerror"); // CSS let songName = document.createTextNode(songs[id].name); btEle.appendChild(songName); aNode.appendChild(btEle); document.getElementById(ids[id]).appendChild(aNode); }; } } let mb = new MusicBox(songs); mb.setSong();
第九話、FileReader
補充一下檔案讀取的IO。讀取文字檔並印出:
function onFileSelect(event) { this.selectedFile = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { const text = reader.result.toString().trim(); console.log(text); } reader.readAsText(this.selectedFile); } document.getElementById('input').addEventListener('change', onFileSelect, false);