2018/01/15

Typescript

TypeScript 是在 MS 工作的 Anders Hejlsberg (C#, TurboPascal 之父)提出的一個新的程式語言,不過他並不是一個無中生有的語言,TypeScript 是 JavaScript ES5 及 ES6 的 superset,可以跟既有的 JavaScript 程式完全相容,他主要是將若資料型別的 JavaScript 轉變為強資料型別的程式語言,在開發及編譯時,就能夠察覺一些程式語法的錯誤,同時增加物件導向的概念,它可以幫助 JavaScript 開發人員更容易撰寫及維護大規模的應用程式。

安裝

TypeScript 的編譯器 (tsc) 可透過 npm 安裝,另外 tsserver 是 node 執行檔,包含了 TypeScript 編譯器及 language service,介面為 JSON protocol,適用於 editors 及 IDE。

$ sudo npm install -g typescript
/opt/local/bin/tsc -> /opt/local/lib/node_modules/typescript/bin/tsc
/opt/local/bin/tsserver -> /opt/local/lib/node_modules/typescript/bin/tsserver
+ typescript@2.6.1
added 1 package in 2.927s

測試:建立一個新的 greeter.ts 檔案

function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

透過 tsc 將 greeter.ts 編譯為 greeter.js

tsc greeter.ts

編譯後的 greeter.js 可在 console 用 nodejs 執行,或是放在一個網頁 greeter.html 裡面

$ node greeter.js
Hello, Jane User

greeter.html

<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src="greeter.js"></script>
    </body>
</html>

開發的 IDE

雖然 Anders Hejlsberg 在官方網頁告訴我們要使用 Visual Studio plugin,但我們還是別的選擇

TypeScript Handbook

翻閱 TypeScript Handbook 手冊會發現,文件的編排方式,就像是在說明一個物件導向的程式語言一樣。

從基本的資料型別 Basic Types 開始,然後說明如何宣告變數,再來就是物件導向的核心: Interface, Classes 及 Functions,最後是 module 與 namespace。

Basic Types

Boolean, Number, String, Array, Tuple, Enum, Any, Void, Null and Undefined, never

// boolean
let isDone: boolean = false;

// number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

// string  " 或是 ' 都可以
let color: string = "blue";
color = 'red';

特殊的 template strings `

let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.

I'll be ${ age + 1 } years old next month.`;

// 等同

let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

Array,有兩種宣告方式

let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3];

Tuple: 就是固定 elements 個數的 array,且各元素的資料型別要相同

// Declare a tuple type
let x: [string, number];

// Initialize it
x = ["hello", 10]; // OK

// Initialize it incorrectly
// error TS2322: Type '[number, string]' is not assignable to type '[string, number]'. Type 'number' is not assignable to type 'string'.
x = [10, "hello"]; // Error

Enum

// 預設第一個元素,數字為 0
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

let colorName: string = Color[2];
alert(colorName); // Displays 'Blue' as it's value is 2 above

// 可強制設定 enum numbers
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

Any: 宣告變數時,不知道這個變數的資料型別是什麼,可以任意變換自己的資料型別

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

類似 Object 的功能,但 Object 只能讓我們指定 value,不能使用該 value 資料型別的任何一個 functions

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'

Void: 通常用在表示 function 沒有 return value

function warnUser(): void {
    alert("This is my warning message");
}

// 將變數宣告為 void,只能設定為 undefined 或是 null
let unusable: void = undefined;

Null and Undefined: 預設 null 及 undefined 是所有其他資料型別的 subtypes,換句話說,可以將 undefined 指定給 number,但如果編譯時加上 --strictNullChecks,就可以限制只能將 null 及 undefined 指定給 void

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

Never: 代表 type of values that never occur,例如可以設定某個只會 throw exception 的 function 的 return value 為 never。他是獨立的,不是任何一種資料型別的 subtype,即使是 any 也不能指定給 never 型別的變數。

// Function returning never must have unreachable end point
function error(message: string): never {
    throw new Error(message);
}

// Inferred return type is never
function fail() {
    return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
    while (true) {
    }
}

Type assertions: 利用 compiler 檢查(確認) 資料型別,有兩種寫法 (someValue) 或是 (someValue as string)

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

let someValue2: any = "this is a string";

let strLength2: number = (someValue as string).length;
Variable Declaration

let 與 var 的差異

JavaScript Hositing: 在 JavaScript,變數可在使用後才被宣告,換句話說,變數可在宣告前,就使用它。在執行時期時,所有var變數都會自動被hoisting。如果程式中有參考到使用var定義過的變數時,會變成undefined,不會產生ERROR。

但 let 宣告的變數,不會被 Hositing,他只能作用在 { } 區塊範圍中。若程式中有參考到let定義過的變數時,因作用區塊不同會產生ERROR,此行為比較接近常用的程式語言寫法。

在 for 裡面,i 會持續被重新定義,所以在 setTimeout 後,最後使用的 i 都會是 5

for (var i = 0; i < 5 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
$ node greeter.js
5
5
5
5
5

let 的變數不會被重新宣告,能夠維持 i 的實際變數 value

for (let i = 0; i < 5 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
$ node greeter.js
0
1
2
3
4

const: 不能被 re-assign 的變數

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error,以 const 宣告的 kitty 是一個有 readonly 屬性的object
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
Functions

typescript 跟 javascript 一樣,可以建立 named 及 anonymous 兩種 functions

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

爲 function 參數及 return value 加上 data type

function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

增加 function type,下面的 (x: number, y: number) => number 就是 myAdd 的 function type,function type 裡面的變數名稱只是輔助使用幫助閱讀而已,實際上跟後面的程式本體沒有關係。

let myAdd: (x: number, y: number) => number =
    function(x: number, y: number): number { return x + y; };
    
let myAdd: (baseValue: number, increment: number) => number =
    function(x: number, y: number): number { return x + y; };

Optional 及 Default Parameters

function 的所有參數,在呼叫該 funciton 時,預設都是必要的。如果是 Optional 參數,要在定義時加上 ? ,也可以為參數寫上 default value

以下這兩個 function 的 function type 都是 (firstName: string, lastName?: string) => string

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

function buildName2(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

Rest Parameters: ... 將剩下的參數集合為一個 array

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this: 在 javascript 的 function 被呼叫時,同時會設定 this 這個變數。但通常 function 會先被定義,而在後面才被呼叫,因此常常會弄錯 this 指定的對象。

Arrow function: ()=> 在 arrow function 中的 this,會指向該 function 定義時的 object,而不是使用該 function 時的 object

foo(x, y) => {
    x++; 
    y--; 
    return x+y;
}

沒有使用arrow function,在呼叫 says 時,setTimeout 裡面的 this 就不是 Animal 而是 window.this

//沒有使用arrow function

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout(function(){
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}

var animal = new Animal()
animal.says('hi')  //undefined says hi

將 function() 改為 () =>

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout( () => {
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}
var animal = new Animal()
animal.says('hi')  //animal says hi

Overloads: 因為 javascript 是非常動態的語言,常常會遇到某個 function 會在不同狀況,回傳不同資料型別的資料。

定義該 function 時會很直覺地將該 function 的 return value 定義為 any。但這樣卻失去了 TypeScript 的強資料型別的檢查功能。

解決方式是在前面明確地將 function 的各種參數及 return 的狀況都定義出來。

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Interfaces

interface 就跟 java interface 功能一樣,可提供 function 定義的檢查,限制 function 必須在實作時,遵循 interface 的定義。

例如 printLabel 需要一個參數 lavelledObj,且該參數要有 label 屬性。

function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

增加一個 interface 定義,讓 labelledObj 定義為 LabelledValue 型別,就能在編譯時,檢查 myObj 是否有遵循 LabelledValue 的介面定義。

interface LabelledValue {
    label: string;
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

Optional interface properties: interface 的屬性,可用 ? 代表該屬性可有可無

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: "black"});

readonly properties: 限制該屬性在 assign 後,就不能被修改

interface Point {
    readonly x: number;
    readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

作用跟 const 很像,差別是 properties 是用 readonly,而 variables 是用 const


Function Types: 可利用 interface 描述 Function Types

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

因 compiler 的檢查機制,可以簡化 function 定義裡面原本要寫的 參數及 return value 的 data types

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

Indexable Types: have an index signature that describes the types we can use to index into the object

interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

Class Types: 就跟 c#, java 的 interface 與 class 的關係一樣,class 可 implements interfaces。interface 可定義 properties 及 functions

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

static 與 instance sides of classes 的差異

class 有兩種面向: static side 與 instance side

如果需要一個特殊的 constructor,以下是有問題的程式,因為當 class 實作 interface 時,只會檢查 instance side,但 constructor 是 static side。

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

必須將 static side 及 instance side 分成兩個 interfaces: ClockConstructor 是給 constructor 用的, ClockInterface 是 instance methods

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
digital.tick();
let analog = createClock(AnalogClock, 7, 32);
analog.tick();

Extending Interfaces: interfaces 可互相 extend,也就是可以從一個 interface 複製 members 到另一個

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Classes

這是最簡單的 class,用 new 語法產生 instance

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

Inheritance: Dog 繼承 Animal,多了 move 這個 method

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

更複雜的例子 Animal: Horse and Snake,用 super 呼叫上層的 method

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

public(預設), private, protected

private: 不能從 class 外面使用該 member protected: 可在 subclass 使用該 member

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// console.log(howard.name); // error

也可以將 constructor 設定為 protected,表示該 class 不能直接被 instantiated,但還是可以被繼承

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee can extend Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
// let john = new Person("John"); // Error: The 'Person' constructor is protected

readonly: 可將 properties 設定為 readonly,但 readonly properties 必須在宣告或是 constructor 初始化

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
// dad.name = "Man with the 3-piece suit"; // error! name is readonly.

可為 readonly 欄位加上 get set methods

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

如果直接編譯會發生 error,可用 tsconfig.json 或是編譯的參數解決

error TS1056: Accessors are only available when targeting ECMAScript 5 and higher.

注意編譯時要加上 --target ES5

tsc --target ES5 greeter.ts

static properties: 直接用 Grid.origin 存取 origin

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

abstract class: 不能直接 instantiated,只有部分已經實作的 methods

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}
Iterators

for..of 及 for..in

for..in 會回傳 a list of keys for..of 會回傳 a list of values

let list = [4, 5, 6];

for (let i in list) {
   console.log(i); // "0", "1", "2",
}

for (let i of list) {
   console.log(i); // "4", "5", "6"
}
Modules

自 ECMAScript 2015 開始,JavaScript 支援了 modules,modules 可讓 variables, functions, classes 運作在 modules 中,外面的程式只能 import 使用 export 的部分。

StringValidator.ts

export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts

import { StringValidator } from "./StringValidator";

const lettersRegexp = /^[A-Za-z]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

ZipCodeValidator.ts

import { StringValidator } from "./StringValidator";

const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// 定義 ZipCodeValidator 時沒有 export,可將 export 獨立寫成一行
//export { ZipCodeValidator };
// export 時,以 as 語法 rename class name
export { ZipCodeValidator as mainValidator };

AllValidators.ts

// 可將其他三個 ts 的 export 合併在一起

export * from "./StringValidator"; // exports interface 'StringValidator'
export * from "./LettersOnlyValidator"; // exports class 'LettersOnlyValidator'
export * from "./ZipCodeValidator";  // exports class 'ZipCodeValidator'

Test.ts

import { StringValidator } from "./StringValidator";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
strings.forEach(s => {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
});
namespaces

namespaces 為 internal modules, modules 為 external modules。

將所有 validator 相關的程式放在 Validation 這個 namespace 裡面,但同樣要用 export 開放使用的介面。

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

Declaration Files

在 TypeScript 要使用 JavaScript 的 libraries,必須要有該 libray 的定義檔

以 jQuery 為例,通常會使用 $('#id') 或是 jQuery('#id') 這樣的語法,在 TypeScript 編譯時,並不知道 $ 或是 jQuery 的意思,這時需要用 declare 語法定義 jQuery。

通常會把 Declaration File 放在獨立的檔案中,例如

// jQuery.d.ts

declare var jQuery: (string) => any;

然後再以 /// 語法引用

/// <reference path="./jQuery.d.ts" />

jQuery('#foo');

完整的 jQuery Declaration File 已經有人寫好了,可以直接下載 DefinitelyTyped/types/jquery/index.d.ts

但在 TypeScript 2.0+ 已經不建議這樣做,而是改用 @types 來管理所有 library 的 Declaration Files,可以用 npm 安裝 jquery 的 @types

npm install --save-dev @types/jquery

使用 jQuery

/// <reference types="jquery" />

$('#id');

編譯

tsc --target ES6 test.ts

如果要搜尋其他 libraries 的定義檔,可在這個網站 DefinitelyTyped: The repository for high quality TypeScript type definitions 搜尋

ref:

How to use jQuery with TypeScript

TypeScript 聲明文件

References

TypeScript新手入門

學習TypeScript:初體驗

我用 TypeScript 語言的七個月

How to use jQuery with TypeScript

Importing jqueryui with Typescript and RequireJS

Adding jQuery and jQueryUI to your TypeScript project


JS ECMAScript 6 compatibility table

沒有留言:

張貼留言