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
編譯後的 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');
編譯
如果要搜尋其他 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