只在此山中,雲深不知處


聽首歌



© 2018 by Shawn Huang
Last Updated: 2018.5.27

TypeScript


設定環境

JavaScript是弱型別語言,TypeScript原則上就是JavaScript,但是是強型別。TypeScript的程式需經過編譯(compile)然後產生JavaScript程式碼,有了JavaScript程式碼即可使用於網頁設計,其好處是可無視瀏覽器型號版本皆可使用。 此處練習的方式,首先開啟Visual Studio Code,開啟新檔案並鍵入以下內容:
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還包含四種無屬性變數: 除此之外,any表示無法確定變數型態,可用為動態變數型態,例如:
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相同,此處介紹一些須注意處。

第三話、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來建立readonly type:
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);