Typescript學習筆記(二)

mylittleZ發表於2019-04-25

泛型

在像C#和Java這樣的語言中,可以使用泛型來建立可重用的元件,一個元件可以支援多種型別的資料。 這樣使用者就可以以自己的資料型別來使用元件。

我們需要一種方法使返回值的型別與傳入引數的型別是相同的。 這裡,我們使用了 型別變數,它是一種特殊的變數,只用於表示型別而不是值。

function identity<T>(arg: T): T {
    return arg;
}
複製程式碼

我們給identity新增了型別變數T。 T幫助我們捕獲使用者傳入的型別(比如:number),之後我們就可以使用這個型別。 之後我們再次使用了 T當做返回值型別。現在我們可以知道引數型別與返回值型別是相同的了。 這允許我們跟蹤函式裡使用的型別的資訊。

我們把這個版本的identity函式叫做泛型,因為它可以適用於多個型別。傳入數值型別並返回數值型別。

宣告泛型方法有以下兩種方式:

function generics_func1<T>(arg: T): T {
    return arg;
}
// 或者
let generics_func2: <T>(arg: T) => T = function (arg) {
    return arg;
}
複製程式碼

我們定義了泛型函式後,可以用兩種方法使用。

第一種是,傳入所有的引數,包含型別引數:

let output = identity<string>("myString");  // type of output will be 'string'
複製程式碼

第二種方法更普遍。利用了型別推論 -- 可以省略型別引數,因為編譯器會根據傳入引數來自動識別對應的型別。

let output = identity("myString");  // type of output will be 'string'
複製程式碼

在方法一的方法體裡,列印了arg引數的length屬性。因為any可以代替任意型別,所以該方法在傳入引數不是陣列或者帶有length屬性物件時,會丟擲異常。而方法二定義了引數型別是Array的泛型型別,肯定會有length屬性,所以不會丟擲異常。但是如果不加array或[]也沒有length屬性。

從下面這個例子可以看出,泛型型別相比較any型別來說,在某些情況下會帶有型別本身的一些資訊,而any型別則沒有。

// 方法一:帶有any引數的方法
function any_func(arg: any): any {
    console.log(arg.length);
    return arg;
}

// 方法二:Array泛型方法
function array_func<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);
    return arg;
}
複製程式碼

泛型型別

泛型介面

interface Generics_interface<T> {
    (arg: T): T;
}

function func_demo<T>(arg: T): T {
    return arg;
}

let func1: Generics_interface<number> = func_demo;
func1(123);     // 正確型別的實際引數
func1('123');   // 錯誤型別的實際引數(因為已經定義為number了)
複製程式碼

通過在介面上宣告泛型,宣告變數時明確指定泛型的具體型別,則賦值的方法將自動帶上具體的型別約束。

泛型型別繼承

interface LengthInterface {
    length: number;
}

function func_demo<T extends LengthInterface>(arg: T): T {
    console.log(arg.length);
    return arg;
}

func_demo({ a: 1, length: 2 });     // 含有length屬性的物件
func_demo([1, 2]);                  // 陣列型別
複製程式碼

上面的例子裡,泛型型別繼承自一個擁有length屬性成員的介面,泛型型別將自動加上length屬性的約束。呼叫時只有符合條件的物件才能正確賦值。

function copy<T extends U, U>(source: U, target: T): T {
    for (let prop in source) {
        target[prop] = source[prop];
    }

    return target;
}

copy({ a: 1, b: 2 }, { a: 2, b: 3, c: 4 });         // 正確的實際引數
copy({ a: 1, b: 2 }, { q: 2, c: 4 });               // 錯誤的實際引數
複製程式碼

在上面的例子裡,一個泛型型別繼承自另外一個泛型型別。在方法呼叫時,就必須確保繼承型別對應的引數物件屬性完全包含被繼承型別對應的引數物件。

泛型類

class Generics_Demo<T>{
    value: T;
    show(): T {
        return this.value;
    }
}

let gene_demo1 = new Generics_Demo<number>();
gene_demo1.value = 1;
console.log(gene_demo1.show());                                     // 呼叫方法

gene_demo1.show = function () { return gene_demo1.value + 1; }      // 賦值新方法,返回值型別必須是number
console.log(gene_demo1.show());
複製程式碼

通過指定明確型別的泛型類的例項,對屬性賦值時,必須滿足實際型別的約束。

列舉

數字列舉

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
複製程式碼

如上,我們定義了一個數字列舉, Up使用初始化為 1。 其餘的成員會從 1開始自動增長。換句話說, Direction.Up的值為 1, Down為 2, Left為 3, Right為 4如果不定義初始值的話就是從0開始。

使用列舉很簡單:通過列舉的屬性來訪問列舉成員,和列舉的名字來訪問列舉型別:

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)
複製程式碼

下面的情況是不被允許的:

enum E {
    A = getSomeValue(),
    B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}
複製程式碼

字串列舉

在一個字串列舉裡,每個成員都必須用字串字面量,或另外一個字串列舉成員進行初始化。

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}
複製程式碼

由於字串列舉沒有自增長的行為,字串列舉可以很好的序列化。 換句話說,如果你正在除錯並且必須要讀一個數字列舉的執行時的值,這個值通常是很難讀的 - 它並不能表達有用的資訊,字串列舉允許你提供一個執行時有意義的並且可讀的值,獨立於列舉成員的名字。

異構列舉

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}
複製程式碼

從技術的角度來說,列舉可以混合字串和數字成員,但是我們不建議這樣做

計算的和常量成員

當滿足如下條件時,列舉成員被當作是常量:

1、它是列舉的第一個成員且沒有初始化器,這種情況下它被賦予值 0:

// E.X is constant:
enum E { X }
複製程式碼

2、它不帶有初始化器且它之前的列舉成員是一個 數字常量。 這種情況下,當前列舉成員的值為它上一個列舉成員的值加1。

// All enum members in 'E1' and 'E2' are constant.

enum E1 { X, Y, Z }

enum E2 {
    A = 1, B, C
}
複製程式碼

3、列舉成員使用 常量列舉表示式初始化。常數列舉表示式是TypeScript表示式的子集,它可以在編譯階段求值。當一個表示式滿足下面條件之一時,它就是一個常量列舉表示式:

一個列舉表示式字面量(主要是字串字面量或數字字面量)

一個對之前定義的常量列舉成員的引用(可以是在不同的列舉型別中定義的)

帶括號的常量列舉表示式

一元運算子 +, -, ~其中之一應用在了常量列舉表示式

常量列舉表示式做為二元運算子 +, -, *, /, %, <<, >>, >>>, &, |, ^的操作物件。 若常數列舉表示式求值後為 NaN或Infinity,則會在編譯階段報錯。

所有其它情況的列舉成員被當作是需要計算得出的值。

enum FileAccess {
    // constant members
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // computed member
    G = "123".length
}
複製程式碼

型別推論

最佳通用型別

當需要從幾個表示式中推斷型別時候,例如

let x = [0, 1, null];
複製程式碼

為了推斷x的型別,我們必須考慮所有元素的型別。 這裡有兩種選擇: number和null。 計算通用型別演算法會考慮所有的候選型別,並給出一個相容所有候選型別的型別。

模組

模組的匯入和匯出

模組在其自身的作用域裡執行,而不是在全域性作用域裡;

這意味著定義在一個模組裡的變數,函式,類等等在模組外部是不可見的,除非你明確地使用export之一匯出它們。

相反,如果想使用其它模組匯出的變數,函式,類,介面等的時候,你必須要匯入它們,可以使用import之一。

模組是自宣告的。在TypeScript裡,兩個模組之間的關係是通過在檔案級別上使用import和export建立的。下面是一個基本例子:

animal.ts

1 export class Animal {
2     name: string;
3     show(): string {
4         return this.name;
5     }
6 }
複製程式碼

app.ts

1 import {Animal} from './animal';
2 let dog = new Animal();
3 dog.name = '狗狗';
4 dog.show();
複製程式碼

上面的例子裡,在animal.ts裡宣告瞭一個類Animal,通過export匯出。在app.ts裡,指定相對檔案路徑,通過import匯入,就可以使用Animal類。

匯入和匯出的重新命名

匯入和匯出時,通過as關鍵字對模組進行重新命名。 animal.ts

class Animal {
    name: string;
    show(): string {
        return this.name;
    }
}

export {Animal as ANI};
複製程式碼

app.ts

import {ANI as Animal} from './animal';
let dog = new Animal();
dog.name = '狗狗';
dog.show();
複製程式碼

匯入和匯出多個物件

MyLargeModule.ts

export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
複製程式碼

Consumer.ts

import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
複製程式碼

相關文章