Wednesday, February 13, 2019

TypeScript 基本語言元素

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

原則上是用在描述函數回傳型別時使用,如果用來宣告變數的型別,該變數將只能繫結 nullundefined 上去。

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

classinterface 中,兩者都可以利用 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 關鍵字來定義,可以使用 thissuper 來參考實體本身以及上層類別中的成員。

成員存取權限跟 Java 很相似,預設是 public,另外有 protectedprivate 可以使用。宣告為 protected 的成員,可以被衍生的類別成員所使用。

如果屬性不希望被更動,可以使用 readonly 修飾字,加上了這個修飾字之後,該屬性只能在建構子中被初始化。

如果要細緻的控制執的存取,可以使用 getset 來程式化屬性的存取,這部份跟 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 相同。也可以使用 abstractclassfunction 上,來建立抽象類別。

由於 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:

  1. 如果該 constructor function 的 prototype 有設定型別 (不是 any) 的話,那就使用該型別;或是
  2. 該 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"
  1. /root/src/moduleB.{ts, tsx, d.ts}
  2. /root/src/moduleB/package.json (設定內的 types 屬性所指定的目標)
  3. /root/src/moduleB/index.{ts, tsx, d.ts}
非相對路徑匯入 (non-relative import; 匯入路徑不是由 /, ./../ 開頭): import { b } from "moduleB"
  1. /root/src/node_modules/moduleB.{ts, tsx, d.ts}
  2. /root/src/node_modules/moduleB/package.json (設定內的 types 屬性所指定的目標)
  3. /root/src/node_modules/moduleB/index.{ts, tsx, d.ts}
  4. /root/node_modules/moduleB.{ts, tsx, d.ts}
  5. /root/node_modules/moduleB/package.json (設定內的 types 屬性所指定的目標)
  6. /root/node_modules/moduleB/index.{ts, tsx, d.ts}
  7. /node_modules/moduleB.{ts, tsx, d.ts}
  8. /node_modules/moduleB/package.json (設定內的 types 屬性所指定的目標)
  9. /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 檔。

No comments: