Microsoft 的文件安排跟 Google 稍微有點不太一樣,在 Tutorial 中的東西非常少,絕大多數都放在 Handbook 中,是蠻有趣的策略。從 URL 上來看,一開始可能甚至沒有打算特別作 Tutorial 的部份。
除了官方文件外,另外有一個 TypeScript Deep Dive 在 Google 搜尋結果上排名也蠻前面的。
測試簡單的語法時,可以使用 TypeScript Playground。看起來分享功能是透過 URL 來做的,所以不能分享太長的程式碼片斷。
語言的規格文件有放在 Github 上面: TypeScript Language Specification
型別標注 (type annotation) 類似一些新的語言,都是放在變數名稱後面:
let username: string;
function func(user: Person) { ... }
interface Name {
firstName: string;
lastName: string;
}
利用 interface
來描述物件的外觀,包含有什麼屬性、方法等。一個物件是不是吻合特定的 interface 會自動的透過檢查內部結構來達到,並不特別需要使用 implements
之類的描述。換言之,跟 Go 一樣,也是 duck typing 的型別系統。
在 class
上,建構子是使用 constructor
關鍵字來宣告。如果建構子參數加上了 public
, protected
, private
(存取範圍修飾子; accessibility modifier) 或是 readonly
關鍵字,那麼稱為 Parameter Properties,參數將會直接成為屬性。
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
基本型別:
- boolean
-
布林值,使用 true
或是 false
表示。
- number
-
跟 JavaScript 相同,所有數值實質上都是浮點數。
定數 (literal) 除了十進位之外,也可以使用 Hex (0xF01D), Binary (0b11010010), Oct (0o0755) 來表示。
- string
-
如同 JavaScript 一樣,可以用雙引號或是單引號來定義字串定數。
另外可以使用 backtick (backquote; `
) 加上 ${ expr }
來定義 Template string:
let sentence: string = `Hello, my name is ${ fullName }.
I'll be ${ age + 1 } years old next month.`;
Template string 可以有很多行,這部份有點類似 Python 的 """ ... """
語法。
- Array
-
陣列可以使用 []
或是泛型陣列語法 (Array<elementType>) 來宣告:
let l: number[] = [1, 2, 3];
let l: Array<number> = [1, 2, 3];
- Tuple
-
數對/數組 (Tuple) 宣告的語法是 let my_tuple: [elementType1, elementType2, ...];
。轉換成 JavaScript 時,是轉換成陣列。
使用元素時,使用類似陣列的方式: my_tuple[1]
。
也可以參照超出原本定義的範圍的元素,型別會被當作所宣告的所有元素型別的聯集型別 (union type),不過取用前需要先賦值。
let x: [string, number] = ["Hello", 18];
x[2].toString(); // Error, 還沒宣告
x[2] = 100; // OK, 100 is (string | number)
x[2].toString(); // OK
x[2].substr(1); // Error, 可使用屬性與方法是 string 與 number 所提供的交集
x[2] = "Today"; // OK, "Today" is (string | number)
x[2] = true; // Error, true is not (string | number)
另外在初始化時,必須同時賦值,至少在 2.7 是這樣,跟對應過去的 JavaScript 程式有關。
也就是說: 這樣寫可以,但是這樣寫不行。
- enum
-
建立列舉的語法是 enum Color {Red, Green, Blue}
,基本上數值代號是依照宣告順序從 0 開始,不過也可以設定值: enum Color {Red = 1, Green, Blue}
。
上面的例子,在 2.7 轉換出來的程式碼是這樣:
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
- any
-
基本上就是任意值,比 Object 類別還廣。
某種層面來說,這是一個脫離 (opt-out) 行別檢查的方式。
- void
-
原則上是用在描述函數回傳型別時使用,如果用來宣告變數的型別,該變數將只能繫結 null
或 undefined
上去。
- null 與 undefined
-
在 TypeScript 中,null 與 undefined 同時也是型別。也就是說,可以宣告一個變數,型別是 null (或是 undefined),不過這樣子這個變數只能設定為 null (或 undefined)。
在預設的組態下,null 與 undefined 是所有型別的子型別。也就是說,可以把 null 或 undefined 賦值給任何型別的變數。
但如果開啟了 --strictNullChecks
檢查,那麼 null 與 undefined 將只會是 void 型別的子型別。在這個情況下,如果要讓一個變數或是參數可以接受 null
為參數,則需要使用 union type 來宣告,比如一個可為空值的字串: string | null
。
使用 --strictNullChecks
是被建議的,可以提早發現一些邏輯錯誤。
- never
-
通常用在宣告一個函式或方法不會正常的結束,在函式或方法的內部邏輯是無窮迴圈或僅會拋出例外時使用。
- Type assertion
-
類似型別轉換,不過轉出來的程式碼事實上並不會進行任何檢查,只是告知編譯器而已,不會有 overhead 在執行時期發生,程式設計師必須確定要轉換的變數確實是所指定的型別。語法有兩種:
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
變數宣告在 TypeScript 中建議多多使用 let
關鍵字,雖然 JavaScript 的 var
也可以用,不過相當雷就是。
|
var |
let |
Scoping |
function |
lexical / block |
Shadowing |
No |
Yes |
Capturing (Closure) |
function |
block (per-iteration) |
比較重要的就是,使用 var 的場合下:
- 變數的 scope 是函數,沒有在管 block level 的,整個命名空間的分割是透過函數作為邊界,在一個函數內重複定義的變數都是繫結在同一個變數實體上。
- 也因為變數的 scope 是函數,所以利用 function 建立 closure 時,變數是繫結在當下的函數空間上。
使用 let 的話:
- 變數的 scope 跟目前流行的語言一樣,是 block level 的,在 inner level 定義的變數可以遮蔽掉 outer level 定義的變數。
- 建立 closure 時,一個 block 一層,不過 loop 比較特殊一點,每個 iteration 會產生一個新的 closure,所以每個
iteration 的計數用變數是不同的 instance。
下面這段程式:
function scope() {
let x: number = 0;
for(let i = 0; i < 3; i++) {
setTimeout(function() { x = x + 1; console.log(`> ${ i }, [${ x }]`); }, 100 * (i + 1));
}
return function() {
console.log(`x = [${ x }]`);
};
}
let f = scope();
setTimeout(f, 500);
會輸出:
> 0, [1]
> 1, [2]
> 2, [3]
x = [3]
宣告常數可以使用 const
關鍵字,用起來跟用 let
基本上是一樣的,不過要注意,如果繫結的目標是一個物件實體的話,只是說所建立的變數參考是不變的,物件實體的內容還是可以被改變。要確保物件實體的內容不改變的話,必須要把屬性加上 readonly
宣告。
在 TypeScript 可以做 parallel assignment,也就是類似 Python 的 unpacking assignment。用法相當的多:
-
let input = [1, 2];
let [first, second] = input;
-
[first, second] = [second, first]; // swap variable
-
function f([first, second]: [number, number]) {
// ...
}
f([1, 2]);
-
let [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4, 5 ]
-
let [first] = [1, 2, 3, 4, 5]; // first = 1, 丟棄其他的數值
-
let [, second, , fourth] = [1, 2, 3, 4]; // second = 2, fourth = 4, 丟棄其他的數值
物件也可以做 destructuring (Object destructuring),感覺就有點誇張了,可能是要用在函數參數上吧!不過這樣傳參數就變成要傳匿名物件,感覺也是怪怪的。
另外也可以做陣列或是物件的融合,稱為 spread:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
// bothPlus 變成 [0, 1, 2, 3, 4, 5]
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
經過 spread 操作後,原始陣列或物件的元素會做一個 shallow copy 到新的陣列或物件中。此外,針對物件的 spread 不會複製方法,只有屬性被複製。
定義 interface 時,可以對屬性加上問號 (?) 讓該屬性變成選用的屬性 (optional property)。
interface SquareConfig {
color?: string;
width?: number;
}
屬性可以加上 readonly
關鍵字,讓該屬性變成唯讀的。
interface Point {
readonly x: number;
readonly y: number;
}
透過 interface
還可以定義函式型別等特殊的型別:
- Function type
-
interface SearchFunc {
(subject_text: string, max_entry_count: number): string[];
}
- Indexable type
-
主要用來限制用來索引的變數型別,在 JavaScript 中預設是可以用任何型別的值來作為陣列的索引。
interface StringArray {
[index: number]: string;
}
混用 number 跟 string 作為索引型別其實也是可行的,不過要注意因為這個情況下 JavaScript 底層會把 number 轉成 string 來作為索引,因此 number 對應的元素型態必須是 string 所對應元素型態的子類別。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
interface Okay {
[x: number]: Dog;
[x: string]: Animal;
}
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
此外,也可以讓陣列成為唯讀的:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
定義 class
的時候,可以透過 implements
關鍵字宣告類別有實作特定的 interface
。
在 class
跟 interface
中,兩者都可以利用 extends
關鍵字來進行繼承,都可以進行多重繼承。
此外,在 interface
上甚至可以繼承 class
,不過在這個情況下,只有屬性與方法的介面定義會被繼承到 interface
上頭,方法的實作不會被繼承。
下面是一些莫名其妙的用法:
-
因為 interface
定義的是 instance 上的限制,因此不可能在針對 instance 的 interface
定義針對建構子的介面定義,因為建構子對所定義的 class
來說是 static 的。
這個問題可以透過額外定義一個內含 new 介面的 interface
來繞過,但就沒辦法很明確的陳述該類別建構子吻合某特定 interface
了,只能透過把 class 物件傳入函式來檢查。
-
因為 JavaScript 的函數本身是 Object,然後 Object 可以隨意的增加屬性,因此可以定義出很奇怪的 interface:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
要造出這種怪東西的實體,可以這樣做:
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
定義 class
時,建構子使用 constructor
關鍵字來定義,可以使用 this
與 super
來參考實體本身以及上層類別中的成員。
成員存取權限跟 Java 很相似,預設是 public
,另外有 protected
與 private
可以使用。宣告為 protected
的成員,可以被衍生的類別成員所使用。
如果屬性不希望被更動,可以使用 readonly
修飾字,加上了這個修飾字之後,該屬性只能在建構子中被初始化。
如果要細緻的控制執的存取,可以使用 get
與 set
來程式化屬性的存取,這部份跟 Dart 的設計類似。
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (newName) {
this._fullName = newName;
} else {
this._fullName = "No name";
}
}
}
另外也可以用 static
來建立靜態成員,語法跟 Java 相同。也可以使用 abstract
在 class
與 function
上,來建立抽象類別。
由於 class 定義最後會被轉換成一個產生 object 的 function 物件,因此有時候會看到在 TypeScript 文件中用 constructor function 這樣的名稱來指稱一個 class 定義。
透過型別推理,下面幾個函式變數的寫法有一樣的結果:
let myAdd: (baseValue: number, increment: number) => number = function(x: number, y: number): number { return x + y; };
let myAdd = function(x: number, y: number): number { return x + y; };
let myAdd: (baseValue: number, increment: number) => number = function(x, y) { return x + y; };
函數的參數可以是選用 (optional) 的:
function buildName(firstName: string, lastName?: string) { ... }
也可以幫參數設定預設值:
function buildName(firstName: string, lastName = "Smith") { ... }
選用的參數必須在非選用的 (required) 參數之後,不過預設值就沒有這個限制。呼叫時把參數值設定為 undefined
就會使用預設值,因此可以不受位置的限制。
如果使用了 --strictNullChecks
參數進行編譯,選用的參數在型別中會自動的被加上 | undefined
成為 union type,這點在選用的類別屬性上也是一樣的。
可以用 ...
來取得剩餘的參數,比較類似 Python 的 *args
的用法:
function buildName(firstName: string, ...restOfName: string[]) { ... }
在 JavaScript 中的 this
會參考到目前的 context 上,也就是函式物件是怎麼呼叫的。文件上的例子是一個物件中的方法傳回了一個函式物件,程式把傳回的函式物件存在全域變數中,此時呼叫這個函式物件會發現他的 this 是 window 這個全域物件實體。
在 TypeScript 中可以利用新的 arrow function 語法,這樣子所建立出來的函式物件會取定義函式物件時的 this 來作為函式物件被呼叫時要使用的 this 參考。
- 傳統的 JavaScript 函式定義:
-
function () { ... }
- TypeScript/ECMAScript6 的 arrow function 函式定義:
-
() => { ... }
基本上,就算是在 TypeScript 中,函式或物件方法仍然是用傳統的 JavaScript function 去看待,因此要注意 this
到底是參考到什麼的問題。
可以加上 this 的型別標注,並在編譯時開啟 --noImplicitThis
編譯選項,來讓編譯器提出關於 this 型別的相關警告。
在沒有特別設定的 function 中,this
型別會預設為 any
,這點即使是物件方法也一樣。加上關於 this
的物件宣告,就可以提示所預期的 this 參考應有的型別。
class Handler {
onClick(this: Handler, e: Event) {
// ...
}
}
但是如果要把物件的方法當成給 DOM event 的 callback function 來使用,要注意是無法正常使用 this 的。因為 callback function 會被當成一般的函式呼叫,所以呼叫時的 this 必須是設定為 void 型別 。換句話說,在 callback function 的原型定義中,this 型別必須為 void 型別。因此,如果要把物件方法當成 callback function 使用,該方法必須特別使用 =>
(arrow function) 來定義:
class Handler {
info: string;
onClick = (e: Event) => {
this.info;
// ...
}
}
要注意的是,每個使用 arrow function 定義出來的函式或方法,背後會帶一包 closure,因此會比傳統的 function 還要佔空間。
TypeScript 也有 function overload,不過看起來只是增加型別檢查的部份,實作上還是要寫一個參數與回傳值都是 any 的函式實體,並在裡面用 typeof
跟 ==
來針對輸入的行別檢查並做出對應反應。
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
if (typeof x == "object") { ... }
else if (typeof x == "number") { ... }
}
以上面的例子來說,最後的 (x): any
實作是不會被算進去的,因此實際上的 overload 只有兩個。
此外,檢查 overload 時,會從給定清單的上面往下面找,因此要把比較明確的放在較上方宣告。
泛型的部份與 Java 很相似,呼叫時的語法大概是 func<string>(arg0, arg1, ...)
這樣,不過也支援型別自動推理 (type argument inference): func("my-string")
來簡略化程式碼。
也支援在泛型型別上增加條件:
function loggingIdentity<T extends Lengthwise>(arg: T): T { ... }
function getProperty<T, K extends keyof T>(obj: T, key: K) { ... }
如果要使用泛型在 factory 函式上,在手冊上有個有點複雜的範例可以參考。
型別推理 (type inference) 基本上發生在賦值時,策略大概有:
- 基本
-
只是純量的賦值,如: let x = 3;
- 最佳共同型別 (best common type)
-
在陣列上時,會儘量找出陣列元素中的共同型別,如果沒有辦法找出共同型別時,會用 union type 來處理。
- 脈絡 (contextual type)
-
如果是賦值到一個已知型別的屬性或是函數呼叫的參數值,會利用該型別來檢查傳入的參數型別。
TypeScript 在物件的型別相容上使用 structural subtyping 作為理論基礎,也就是說,物件實體是不是等同於或相容於某個型別,是透過實體上有哪些屬性與方法來決定。
部份無法在 compile time 確認是型別安全的操作在 TypeScript 是被允許的,因此 TypeScript 學理上是 not soundness 的語言。從例子看起來,主要是有 callback 的狀況下會有允許 unsound 的設計。
比如這樣一個函式:
function listenEvent(eventType: EventType, handler: (n: Event) => void) { ... }
下面這樣呼叫是 Unsound 的,因為 MouseEvent 比 Event 還嚴格,但是基於 assignment 的角度來檢查型別,所以這樣寫還是會過,而且這樣寫也比較好懂:
listenEvent(EventType.Mouse, (e: MouseEvent) => { ... });
如果要維持 Soundness 的話,可以這樣寫,但是這樣寫就比較不容易閱讀:
listenEvent(EventType.Mouse, (e: Event) => { ... ((<MouseEvent>e).x + "," + (<MouseEvent>e).y)); ... });
列舉 (enum) 跟數值 (number) 是可以互相相容的,不過不同的 enum 之間是不相容的。
類別 (class) 的型別相容檢查只針對 instance 上的屬性與方法檢查,建構子與 static member 不在檢查範圍內。類別實體檢查型別相容性時,會額外考慮 private 與 protected 的成員,檢查目標也要有同樣的成員,而且必須是在同一個類別定義的,這樣的特性讓子類別可以相容於上層類別,但不會與不同繼承關係下的其他類別實體相容。
Intersection type 語法是 T & U & V ...
這樣寫,代表這個融合後型別的實體必須有「所有」基礎類別的屬性與方法。對應到其他語言會是 Mixin 的特性,不過看起來 TypeScript 本身並沒有特別有 Mixin 的支援,至少範例裡面把兩個類別實作融在一起的部份是另外寫迴圈做的。
Union type 語法是 T | U | V ...
這樣寫,代表融合的型別只需要有基礎類別中「共同」的屬性與方法,常用在參數可以接受兩種以上型別時。
使用 union type 時可以搭配稱為 type guard 的 if 語法,來簡化程式碼。原本要利用 type assertion 的程式碼:
let pet: Fish | Bird = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
可以透過特定的型別檢查判斷式,搭配 if ... else if ... else
分支,在 if 內的程式碼區塊中,變數的型別會被局部的窄化 (narrow) 為所檢查通過的型別。
型別檢查的判斷式有幾種型式:
- 使用者定義的檢查 (type predicate)
-
利用撰寫一個傳回 type predicate 的函式,這個函式的參數型別必須是要檢查的 union type,傳回一個型別為 type predicate 的布林值:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
這樣子可以自行撰寫型別檢查的邏輯,可以做類似 feature-based 的分支。
- typeof
-
透過 typeof 關鍵字可以取得物件實體型別的字串表示,可以透過判斷型別名稱字串來做型別的檢查,但是只有非常有限的型別被支援。
支援的檢查陳述式為 typeof v === "typename<"
或 typeof v !== "typename<"
兩種,支援的型別 (typename) 有: number
, string
, boolean
, symbol
,其他型別的字串也可以寫,但是不會有 type guard 的效果。
使用範例如下:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
...
} else if (typeof padding === "string") {
...
}
...
}
- instanceof
-
也可以使用 instanceof 來進行 type guard:
if (pet instanceof Fish) {
pet.swim();
}
else {
pet.fly();
}
在 instanceof
的右側必須要是一個 constructor function,程式碼區塊內會把物件實體變數的型別依照下面邏輯順序判斷進行 narrow:
- 如果該 constructor function 的
prototype
有設定型別 (不是 any
) 的話,那就使用該型別;或是
- 該 constructor function 所定義的所有可回傳型別的 union type。
- 針對 null 的處理
-
如果變數是 null 的 union type 的話,可以利用 v == null
來進行 type guard:
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
看起來大概就是: 如果要做基本型別的 type guard 可以使用 typeof
;如果是類別的話,則使用 instanceof
來進行。
對於 Nullable type (也就是有 null 或 undefined 一起作為 union type 的變數) 在除去 null 的時候可以利用 terser operator: x = x || "default";
當編譯器無法自動追蹤出變數已經不可能為 null 或 undefined 值時,可以在該變數識別字後加上驚嘆號 (type assertion operator): title = name!.charAt(1) ...
告訴編譯器該值已不可能為空值。
使用 type
關鍵字可以定義型別的別名 (type alias),類似 C/C++ 的 typedef
關鍵字。型別別名在編譯後會被展開,此外也不能進行 extends
或是 implements
等擴充。
可以在 type alias 中使用泛型或是參考到自己:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
或是搭配 intersection type 使用:
type LinkedList<T> = T & { next: LinkedList<T> };
字串或是數值的定數 (literal) 也是型別,稱為 literal type,可以搭配 union type 或 type alias 等特性使用。
比如對於 type alias: type Easing = "ease-in" | "ease-out" | "ease-in-out";
如果有變數宣告為 Easing 型別,那麼該變數中可以接受的值就只有所指定的字串。
另外也可以搭配 overload 使用:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// overloads ...
function createElement(tagName: string): Element {
// 實作
}
Literal type 跟 enum member type 合稱為 singleton type,不過常會不精準的混用。
類別的方法可以用 this
當成傳回型別,這樣被繼承之後,舊的方法傳回來的物件參考還是新的類別型別,可以便利 chained invoke 的程式撰寫方法。
因為 JavaScript 物件可以用類似關聯陣列的方式存取,因此也有相關的 operator 可以使用,相關的 feature 稱為 index type。
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // ERR: 'unknown' is not in 'name' | 'age'
透過 keyof T
可以將給定的型別 T 中所有的 public 屬性與方法的名稱取出並構成 union type,這個操作稱為 index type query operator。要特別注意,是取出為型別 (union type) 而不是一個字串陣列。
取得類別中特定屬性的型別可以用 T[K]
運算子,會需要搭配 K extends keyof T
來使用,這個運算子稱為 indexed access operator,可以取出 T 類別中 K 所指涉的屬性或方法的型別。
感覺上在終端的程式上 index type 相關 operator 好像沒辦法使用,但是在 library 或 framework 上應該就可以用上,來跟終端程式碼的自訂型別進行整合。
還可以透過 mapped type 來將所有的 public 成員型別做一個系統性的變更:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
type Nullable<T> = {
[P in keyof T]: T[P] | null;
}
上面這幾個算是常用的,有納入標準函式庫中: ECMAScript APIs。
在 ECMAScript 2015 增加了 Symbol 建構函式跟 symbol 型別,看起來是用來建立匿名的識別字 (identifier): MDN: Glossary - Symbol, TypeScript Handbook: Symbols。
除了一般的 for (...; ...; ...)
之外,迴圈還有 for (... of ...)
跟 for (... in ...)
兩種,分別是對 container 的元素本身,以及索引鍵值遊走。
在 Handbook 上針對元素遊走 (for-of) 有特別提到,因為是比較新的 ES6 的語法,如果 TypeScript 的 target 是設定為 ES5 或更早版本的話,只能用在 Array 型別上。不過,在 2.3 的 release node 裡頭好像又說目前可以轉換的程度有比較厲害,所以看來只能用試的了。
在 MDN 的文章對於 Iterator 的說明比較清楚: Symbol.iterator。
要使用的話可以參考 TypeScript Deep Dive: Iterator 章節,有實作上的細節。
有 import
或是 export
的程式碼檔案,就自動會被視為 module 看待。沒有 import
或是 export
的程式碼檔案,定義的內容就會當成全域的內容,對所有程式碼與模組開放。模組內的變數或方法必須要有 export
修飾字才可以被其他模組 import
引用,可以一次匯入或匯出複數個變數或方法,匯入時可以利用 as
設定新的名稱。
如果一個模組只 export 了一個參考,可以使用 export default
的方式,程式會比較清楚。這樣匯出的參考,在匯入時一律需要給定別名。
在模組內可以定義 namespace
來在內部切割命名空間,另外也可以搭配 /// <reference path="code-file.ts" />
來匯入。
因為 JavaScript 的模組化實在太多嘗試了,所以感覺這部份 TypeScript 也有點亂,有各種相容語法。
使用 import ... from ...
時,搜尋模組有 Classic 以及 Node 兩種模式,原則上在使用 Angular 時應該是使用 Node 模式。
以在 /root/src/moduleA.ts 匯入為例:
- 相對路徑匯入 (relative import; 匯入路徑由
/
, ./
或 ../
開頭): import { b } from "./moduleB"
-
/root/src/moduleB.{ts, tsx, d.ts}
/root/src/moduleB/package.json
(設定內的 types
屬性所指定的目標)
/root/src/moduleB/index.{ts, tsx, d.ts}
- 非相對路徑匯入 (non-relative import; 匯入路徑不是由
/
, ./
或 ../
開頭): import { b } from "moduleB"
-
/root/src/node_modules/moduleB.{ts, tsx, d.ts}
/root/src/node_modules/moduleB/package.json
(設定內的 types
屬性所指定的目標)
/root/src/node_modules/moduleB/index.{ts, tsx, d.ts}
/root/node_modules/moduleB.{ts, tsx, d.ts}
/root/node_modules/moduleB/package.json
(設定內的 types
屬性所指定的目標)
/root/node_modules/moduleB/index.{ts, tsx, d.ts}
/node_modules/moduleB.{ts, tsx, d.ts}
/node_modules/moduleB/package.json
(設定內的 types
屬性所指定的目標)
/node_modules/moduleB/index.{ts, tsx, d.ts}
以程式碼所在資料夾為基礎無法找到時會往上層找,這部份是模擬 Node.js 的行為,依據 Node.js 的文件是會找到根目錄為止。
Triple-slash directives 就是指檔案最上頭的 /// <... />
那個指引,比較會用到的應該就:
/// <reference path="..." />
-
主要是用來宣告相依順序,雖然有點類似 C/C++ 的 #include ...
但是實際參考到的檔案並不會在輸出中出現。
也就是說,如果參考到的檔案也希望一起編到輸出的 .js
檔的話,輸入檔案清單也要提到那些檔案,產生 JavaScript 程式碼時也會一句這邊的設定決定輸出的程式碼區塊的先後順序。
這個指引使用的路徑基本上是檔案相對路徑,應該是以模組內的使用為主。
/// <reference types="..." />
-
主要是用來參考自行撰寫的 .d.ts
用,會轉換為 @types/.../index.d.ts
後,用類似 module 的找尋邏輯去找。
需要搭配 tsconfig.json 中的設定使用: @types, typeRoots and types。
使用 tsconfig.json 可以設定要給 compiler (tsc
) 的相關組態設定,當執行 tsc 但沒給輸入的程式碼檔案時,就會從目前工作資料夾開始找 tsconfig.json 來使用,找不到時會往上層資料夾去找。另外也可以使用 -p 或 --project 參數,可以接受資料夾或是一個 JSON 檔案路徑,當給定資料夾時會去找該資料夾下的 tsconfig.json 檔。