Typescript學習總結


typescript(以下簡稱TS)出來也有好長時間了,下面記錄一下學習心得。

首先學這門語言前,請確保有以下基礎知識:

  • 扎實的javascript基礎知識
  • es6的基礎知識
  • 面向對象編程的概念(沒有也可以,就當是重新學一遍了)

接下來看一下TS的一些概念:

一、基本類型

TS的基礎類型有:字符串(string)、數字(number)、布爾值(boolean)、空(null)、未定義(undefined)、數組(array)、對象(object)、元組(tuple)、枚舉(enum)、any、void、never等12種。

寫法為在變量后加冒號然后跟變量類型的方式,例如:

1.字符串

寫法:

let str: string = 'str';

2.數字

寫法:

let num: number = 123;

3.布爾值

寫法:

let bol: boolean = false;

4.null

寫法:

let n: null = null;

5.undefined

寫法:

let u: undefined = undefined;

6.數組

寫法:

let arr: number[] = [1,23,4,];
let arr1: Array<number> = [1,2,3];// 使用泛型的方式聲明變量

7.對象

寫法:

let obj: object={};

8.元組

寫法:

let tuple: [number,string] = [12,'3'];

9.枚舉

寫法:

enum Num{
  one=1,// 從幾開始,默認為從0開始
  two,// 2
  three// 3
};

10.any

寫法:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
let anyArr: any = [1,2,'4',false,null];

11.viod

寫法:

function warnUser(): void {
    console.log("This is my warning message");
}
let unusable: void = undefined;
let unuse: void;

12.never

寫法:

function error(message: string): never {
    throw new Error(message);
}
// 推斷的返回值類型為never
function fail() {
    return error("Something failed");
}
// 返回never的函數必須存在無法達到的終點
function infiniteLoop(): never {
    while (true) {
    }
}

PS:類型斷言:如果你很清楚一個變量比它現有類型更確切的類型,那么你可以使用類型斷言。

類型斷言有兩種形式:

1.尖括號寫法:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

2.As寫法:

let someValueTwo: any = "this is a string";
let strLengthTwo: number = (someValueTwo as string).length;

當在TypeScript里使用JSX時,只能使用As語法斷言。

2、接口

TypeScript的核心原則之一是對值所具有的結構進行類型檢查。 它有時被稱做“鴨式辨型法”或“結構性子類型化”。 在TypeScript里,接口的作用就是為這些類型命名定義契約。

寫法:

interface 接口名 { attribute: type }

示例:

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

1.可選屬性

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

2.只讀屬性

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

3.只讀數組

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
// a = ro as number[]; 用斷言修改數組為可修改!

 4.跳過額外的屬性檢查

interface SquareConfig {
    color?: string;
    width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
    return {
        color: 'blue',
        area:23
    }
    // ...
}
(方法1)
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
(方法2)索引簽名
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}
(方法3)將這個對象賦值給一個另一個變量: 因為squareOptions不會經過額外屬性檢查
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

5.通過接口定義函數類型

interface SearchFunc {
    (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}
// or
// mySearch = function(src: string, sub: string): boolean {
//     let result = src.search(sub);
//     return result > -1;
// }

6.可索引的類型

TypeScript支持兩種索引簽名:字符串和數字。

可以同時使用兩種類型的索引,但是數字索引的返回值必須是字符串索引返回值類型的子類型。 這是因為當使用number來索引時,JavaScript會將它轉換成string然后再去索引對象。 也就是說用 100(一個number)去索引等同於使用"100"(一個string)去索引,因此兩者需要保持一致。

interface StringArray {
    [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
// 定義的StringArray接口,它具有索引簽名,表示當用number去索引StringArray時會得到string類型的返回值。
interface NumberDictionary {
    [index: string]: number;
    length: number;    // 可以,length是number類型
    name: string       // 錯誤,`name`的類型與索引類型返回值的類型不匹配
}
// 將索引簽名設置為只讀
interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

7.實現接口

TypeScript也能夠用它來明確的強制一個類去符合某種契約

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

類靜態部分與實例部分的區別

當你操作類和接口的時候,你要知道類是具有兩個類型的:靜態部分的類型和實例的類型。 你會注意到,當你用構造器簽名去定義一個接口並試圖定義一個類去實現這個接口時會得到一個錯誤:

interface ClockConstructor {
    new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}
// 這里因為當一個類實現了一個接口時,只對其實例部分進行類型檢查。 constructor存在於類的靜態部分,所以不在檢查的范圍內。
// 因此,我們應該直接操作類的靜態部分。 看下面的例子,我們定義了兩個接口, ClockConstructor為構造函數所用和ClockInterface為實例方法所用。 為了方便我們定義一個構造函數 createClock,它用傳入的類型創建實例。
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);
let analog = createClock(AnalogClock, 7, 32);
// 因為createClock的第一個參數是ClockConstructor類型,在createClock(AnalogClock, 7, 32)里,會檢查AnalogClock是否符合構造函數簽名。

8.繼承接口

和類一樣,接口也可以相互繼承。 這讓我們能夠從一個接口里復制成員到另一個接口里,可以更靈活地將接口分割到可重用的模塊里。

interface Shape {
    color: string;
}
interface Square extends Shape {
    sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

繼承多個接口:

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;

9.混合類型

一個對象可以同時做為函數和對象使用,並帶有額外的屬性。

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
function getCounter(): Counter {
    let counter = <Counter>function (start: number) { console.log(start) };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

10.接口繼承類

當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現。就好像接口聲明了所有類中存在的成員,但並沒有提供具體實現一樣。 接口同樣會繼承到類的private和protected成員。 這意味着當你創建了一個接口繼承了一個擁有私有或受保護的成員的類時,這個接口類型只能被這個類或其子類所實現(implement)。

當你有一個龐大的繼承結構時這很有用,但要指出的是你的代碼只在子類擁有特定屬性時起作用。 這個子類除了繼承至基類外與基類沒有任何關系。 例:

class Control {
    private state: any;
}
interface SelectableControl extends Control {
    select(): void;
}
class Button extends Control implements SelectableControl {
    select() { }
}
class TextBox extends Control {
    select() { }
}
// 錯誤:“Image”類型缺少“state”屬性。
class Image implements SelectableControl {
    select() { }
}
class Location {

}
// 在上面的例子里,SelectableControl包含了Control的所有成員,包括私有成員state。 因為state是私有成員,所以只能夠是Control的子類們才能實現SelectableControl接口。 
因為只有 Control的子類才能夠擁有一個聲明於Control的私有成員state,這對私有成員的兼容性是必需的。
// 在Control類內部,是允許通過SelectableControl的實例來訪問私有成員state的。 實際上, SelectableControl接口和擁有select方法的Control類是一樣的。
Button和TextBox類是SelectableControl的子類(因為它們都繼承自Control並有select方法),但Image和Location類並不是這樣的。

 3.TS類

從ECMAScript 2015,也就是ES 6開始,JavaScript程序員將能夠使用基於類的面向對象的方式。

1.類聲明

class CreateClass {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
let greeter = new CreateClass('demo');

2.類繼承

2.1類繼承:類從基類中繼承了屬性和方法。這里,Dog是一個派生類,它派生自ParentClass基類,通過extends關鍵字。派生類通常被稱作子類,基類通常被稱作超類。

class ParentClass {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}
class Dog extends ParentClass {
    bark() {
        console.log('Woof! Woof!');
    }
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

2.2類私有屬性:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
    constructor() { super("Rhino"); }
}
class Employee1 {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee1("Bob");
console.log(animal.name); // 錯誤

2.3類受保護屬性:

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); // 錯誤

構造函數也可以被標記成 protected。 這意味着這個類不能在包含它的類外被實例化,但是能被繼承。比如:

class Person2 {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}
// Employee 能夠繼承 Person
class Employee2 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 howard2 = new Employee2("Howard", "Sales");
let john = new Person2("John"); // 錯誤: 'Person' 的構造函數是被保護的.

2.4靜態屬性:類的靜態成員,這些屬性存在於類本身上面而不是類的實例上。

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) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

2.5抽象類:抽象類做為其它派生類的基類使用。 它們一般不會直接被實例化。

abstract class AbstractClass {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象類中的抽象方法不包含具體實現且必須在派生類中實現。 抽象方法的語法與接口方法相似。 兩者都是定義方法簽名但不包含方法體。 然而,抽象方法必須包含 abstract關鍵字並且可以包含訪問修飾符。

abstract class Department {
    constructor(public name: string) {
    }
    printName(): void {
        console.log('Department name: ' + this.name);
    }
    abstract printMeeting(): void; // 必須在派生類中實現
}
class AccountingDepartment extends Department {
    constructor() {
        super('Accounting and Auditing'); // 在派生類的構造函數中必須調用 super()
    }
    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }
    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}
let department: Department; // 允許創建一個對抽象類型的引用
department = new Department(); // 錯誤: 不能創建一個抽象類的實例
department = new AccountingDepartment(); // 允許對一個抽象子類進行實例化和賦值
department.printName();
department.printMeeting();
department.generateReports(); // 錯誤: 方法在聲明的抽象類中不存在

2.6類當做接口使用

class Point {
    x: number;
    y: number;
}
interface Point3d extends Point {
    z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};

4.泛型(generic)

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

1.泛型類型:與非泛型函數的類型沒什么不同,只是有一個類型參數在最前面,像函數聲明一樣:

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <T>(arg: T) => T = identity; // or
let myIdentity1: {<T>(arg: T): T} = identity;
// 這引導我們去寫第一個泛型接口了。 我們把上面例子里的對象字面量拿出來做為一個接口
interface GenericIdentityFn {
    <T>(arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;

2.泛型類

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
// GenericNumber類的使用是十分直觀的,並且你可能已經注意到了,沒有什么去限制它只能使用number類型。 也可以使用字符串或其它更復雜的類型。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

5.枚舉

1.數字枚舉

enum Direction {
    Up = 1, // 使用初始值,遞增,否則默認從0開始
    Down,
    Left,
    Right
}

2.字符串枚舉

enum Direction2 {
    Up = "UP", // 每個字符串枚舉成員必須進行初始化
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

3.異構枚舉(可以混合字符串和數字成員)

例1:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

例2:

enum E {
    Foo,
    Bar,
}
function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
        //             ~~~~~~~~~~~
        // Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'.
    }
}

4.聯合枚舉與枚舉成員的類型

enum ShapeKind {
    Circle,
    Square,
}
interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}
interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}
let c11: Circle = {
    kind: ShapeKind.Square, // 正確的為ShapeKind.Circle
    //    ~~~~~~~~~~~~~~~~ Error!
    radius: 100,
}

5.運行時枚舉

枚舉是在運行時真正存在的對象

enum E {
    X, Y, Z
}
function f(obj: { X: number }) {
    console.log('X',obj.X);
    return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);

6.反向映射

除了創建一個以屬性名做為對象成員的對象之外,數字枚舉成員還具有了反向映射

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
// 生成的代碼中,枚舉類型被編譯成一個對象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚舉成員總會生成為對屬性訪問並且永遠也不會內聯代碼。
// 要注意的是 不會為字符串枚舉成員生成反向映射,因為枚舉成員不能具有數值名,所以數字枚舉成員具有反射

7.常量(const)枚舉

為了避免在額外生成的代碼上的開銷和額外的非直接的對枚舉成員的訪問,我們可以使用 const枚舉。 常量枚舉通過在枚舉上使用 const修飾符來定義。

常量枚舉注意點:

1.不會生成反向映射

2.不能直接訪問值

const enum Order {
    A,
    B,
    C,
}

8.外部枚舉

外部枚舉用來描述已經存在的枚舉類型的形狀,簡單理解就是方便用戶編寫函數時的提示

declare enum Enum {
    A = 1,
    B,
    C = 2
}

外部枚舉和非外部枚舉之間有一個重要的區別,在正常的枚舉里,沒有初始化方法的成員被當成常數成員。 對於非常數的外部枚舉而言,沒有初始化方法時被當做需要經過計算的。

用來描述一個應該存在的枚舉類型的,而不是已經存在的,它的值在編譯時不存在,只有等到運行時才知道。

6.模塊

TypeScript 1.5里術語名已經發生了變化。 “內部模塊”現在稱做“命名空間”。 “外部模塊”現在則簡稱為“模塊”,這是為了與 ECMAScript 2015里的術語保持一致,(也就是說 module X { 相當於現在推薦的寫法 namespace X {)。

1.導出

變量,函數,類,類型別名或接口都可以通過export導出

導出聲明

export interface StringValidator {
    isAcceptable(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

導出語句

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

7.高級類型

1.交叉類型:多個類型合並為一個類型

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}
class Person1 {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
        return 11;
    }
}
var jim = extend(new Person1("Jim"), new ConsoleLogger());
var n1 = jim.name;
jim.log();

2.聯合類型

聯合類型表示一個值可以是幾種類型之一。用豎線( | )分隔每個類型,所以 number | string | boolean表示一個值可以是 number, string,或 boolean。

function padLeft(value: string, padding: string | number | boolean) {
    // ...
}
let indentedString = padLeft("Hello world", true); 
// 如果一個值是聯合類型,我們只能訪問此聯合類型的所有類型里共有的成員。
interface Bird {
    fly();
    layEggs();
}
interface Fish {
    swim();
    layEggs();
}
function getSmallPet(): Fish | Bird {
    // ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

3.類型保護

let pet = getSmallPet();
if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

1.自定義類型保護

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}
// 'swim' 和 'fly' 調用都沒有問題了
if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

2.typeof類型保護

function isNumber(x: any): x is number {
    return typeof x === "number";
}
function isString(x: any): x is string {
    return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

3.instanceof類型保護

interface Padder {
    getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}
class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}
function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}
// 類型為SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
    padder; // 類型細化為'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 類型細化為'StringPadder'
}

4.可以為null的類型

let s = "foo";
s = null; // 錯誤, 'null'不能賦值給'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能賦值給'string | null'

5.可選參數和可選屬性

使用了 --strictNullChecks,可選參數會被自動地加上 | undefined。

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
// 可選屬性也會有同樣的處理:
class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

6.類型斷言

可以為null的類型是通過聯合類型實現,那么你需要使用類型保護來去除 null。

如果編譯器不能夠去除 null或 undefined,你可以使用類型斷言手動去除。 語法是添加 !后綴: identifier!從 identifier的類型里去除了 null和 undefined:

function broken(name: string | null): string {
    function postfix(epithet: string) {
      return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
    }
    name = name || "Bob";
    return postfix("great");
}
function fixed(name: string | null): string {
    function postfix(epithet: string) {
      return name!.charAt(0) + '.  the ' + epithet; // ok
    }
    name = name || "Bob";
    return postfix("great");
}

本例使用了嵌套函數,因為編譯器無法去除嵌套函數的null(除非是立即調用的函數表達式)。 因為它無法跟蹤所有對嵌套函數的調用,尤其是你將內層函數做為外層函數的返回值。 如果無法知道函數在哪里被調用,就無法知道調用時 name的類型。

7.類型別名

類型別名會給一個類型起個新名字。 類型別名有時和接口很像,但是可以作用於原始值,聯合類型,元組以及其它任何你需要手寫的類型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}
// 起別名不會新建一個類型 - 它創建了一個新名字來引用那個類型。 給原始類型起別名通常沒什么用,盡管可以做為文檔的一種形式使用。
// 同接口一樣,類型別名也可以是泛型 - 我們可以添加類型參數並且在別名聲明的右側傳入:
type Container<T> = { value: T };
// 我們也可以使用類型別名來在屬性里引用自己:
type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}
// 與交叉類型一起使用,我們可以創建出一些十分稀奇古怪的類型。
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
    name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
// 然而,類型別名不能出現在聲明右側的任何地方。
type Yikes = Array<Yikes>; // error

8.接口 & 類型別名

接口創建了一個新的名字,可以在其它任何地方使用。類型別名並不創建新名字—比如,錯誤信息就不會使用別名。 在下面的示例代碼里,在編譯器中將鼠標懸停在 interfaced上,顯示它返回的是 Interface,但懸停在 aliased上時,顯示的卻是對象字面量類型。

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

類型別名不能被 extends和 implements(自己也不能 extends和 implements其它類型)。 因為軟件中的對象應該對於擴展是開放的,但是對於修改是封閉的,你應該盡量去使用接口代替類型別名。

如果你無法通過接口來描述一個類型並且需要使用聯合類型或元組類型,這時通常會使用類型別名。

9.字符串字面量類型

字符串字面量類型允許你指定字符串必須的固定值。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
// 字符串字面量類型還可以用於區分函數重載:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element { }

10.數字字面量類型

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
    return 1;
}
function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

11.可辨識聯合

你可以合並單例類型、聯合類型、類型保護和類型別名來創建一個叫做【可辨識聯合的高級模式】,它也稱做【標簽聯合】或【代數數據類型】。可辨識聯合在函數式編程很有用處。一些語言會自動地為你辨識聯合;而TypeScript則基於已有的JavaScript模式。它具有3個要素:

  1. 具有普通的單例類型屬性 — 可辨識的特征。
  2. 一個類型別名包含了那些類型的聯合 — 聯合。
  3. 此屬性上的類型保護。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

首先我們聲明了將要聯合的接口。每個接口都有kind屬性但有不同的字符串字面量類型。kind屬性稱做可辨識的特征或標簽。其它的屬性則特定於各個接口。注意,目前各個接口間是沒有聯系的。下面我們把它們聯合到一起:

type Shape = Square | Rectangle | Circle;
// 現在我們使用可辨識聯合:
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

12.完整性約束

當沒有涵蓋所有可辨識聯合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們添加了 Triangle到 Shape,我們同時還需要更新 area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

有兩種方式可以實現。

1.啟用 --strictNullChecks並且指定一個返回值類型:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

因為 switch沒有包涵所有情況,所以TypeScript認為這個函數有時候會返回 undefined。 如果你明確地指定了返回值類型為 number,那么你會看到一個錯誤,因為實際上返回值的類型為 number | undefined。 然而,這種方法存在些微妙之處且 --strictNullChecks對舊代碼支持不好。

2.使用 never類型,編譯器用它來進行完整性檢查

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

這里, assertNever檢查 s是否為 never類型—即為除去所有可能情況后剩下的類型。 如果你忘記了某個case,那么 s將具有一個真實的類型並且你會得到一個錯誤。 這種方式需要你定義一個額外的函數,但是在你忘記某個case的時候也更加明顯。

13.多態的this類型

多態的this類型表示的是某個包含類或接口的子類型。這被稱做F-bounded多態性。它能很容易的表現連貫接口間的繼承,比如。在計算器的例子里,在每個操作之后都返回this類型:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}
let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

由於這個類使用了 this類型,你可以繼承它,新的類可以直接使用之前的方法,不需要做任何的改變。

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}
let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

如果沒有this類型,ScientificCalculator就不能夠在繼承BasicCalculator的同時還保持接口的連貫性。multiply將會返回BasicCalculator,它並沒有sin方法。然而,使用this類型,multiply會返回this,在這里就是ScientificCalculator。

14.索引類型

使用索引類型,編譯器就能夠檢查使用了動態屬性名的代碼。例如,一個常見的JavaScript模式是從對象中選取屬性的子集。

function pluck(o, names) {
    return names.map(n => o[n]);
}

下面是如何在TypeScript里使用此函數,通過 索引類型查詢和 索引訪問操作符:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}
interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

編譯器會檢查 name是否真的是 Person的一個屬性。 本例還引入了幾個新的類型操作符。 首先是 keyof T, 索引類型查詢操作符。 對於任何類型 T, keyof T的結果為 T上已知的公共屬性名的聯合。 例如:

let personProps: keyof Person; // 'name' | 'age'
// keyof Person是完全可以與 'name' | 'age'互相替換的。 不同的是如果你添加了其它的屬性到 Person,例如 address: string,那么 keyof Person會自動變為 'name' | 'age' | 'address'。 
你可以在像 pluck函數這類上下文里使用 keyof,因為在使用之前你並不清楚可能出現的屬性名。 但編譯器會檢查你是否傳入了正確的屬性名給 pluck:
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age' // 第二個操作符是 T[K], 索引訪問操作符。 在這里,類型語法反映了表達式語法。 這意味着 person['name']具有類型 Person['name'] — 在我們的例子里則為 string類型。
然而,就像索引類型查詢一樣,你可以在普通的上下文里使用 T[K],這正是它的強大所在。 你只要確保類型變量 K extends keyof T就可以了。 例如下面 getProperty函數的例子:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] } // getProperty里的 o: T和 name: K,意味着 o[name]: T[K]。 當你返回 T[K]的結果,編譯器會實例化鍵的真實類型,因此 getProperty的返回值類型會隨着你需要的屬性改變。 let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

15.索引類型和字符串索引簽名

keyof和 T[K]與字符串索引簽名進行交互。 如果你有一個帶有字符串索引簽名的類型,那么 keyof T會是 string。 並且 T[string]為索引簽名的類型:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

16.映射類型

映射類型指從舊類型中創建新類型

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}
// 像下面這樣使用:
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

最簡單的映射類型和它的組成部分:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的語法與索引簽名的語法類型,內部使用了 for .. in。 具有三個部分:

  1. 類型變量 K,它會依次綁定到每個屬性。
  2. 字符串字面量聯合的 Keys,它包含了要迭代的屬性名的集合。
  3. 屬性的結果類型。

在個簡單的例子里, Keys是硬編碼的的屬性名列表並且屬性類型永遠是 boolean,因此這個映射類型等同於:

type Flags = {
    option1: boolean;
    option2: boolean;
}

在真正的應用里,可能不同於上面的 Readonly或 Partial。 它們會基於一些已存在的類型,且按照一定的方式轉換字段。 這就是 keyof和索引訪問類型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
// 但它更有用的地方是可以有一些通用版本。
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

在這些例子里,屬性列表是 keyof T且結果類型是 T[P]的變體。 這是使用通用映射類型的一個好模版。 因為這類轉換是 同態的,映射只作用於 T的屬性而沒有其它的。 編譯器知道在添加任何新屬性之前可以拷貝所有存在的屬性修飾符。 例如,假設 Person.name是只讀的,那么 Partial<Person>.name也將是只讀的且為可選的。

下面是另一個例子, T[P]被包裝在 Proxy<T>類里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

注意 Readonly<T>和 Partial<T>用處不小,因此它們與 Pick和 Record一同被包含進了TypeScript的標准庫里:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}
// Readonly, Partial和 Pick是同態的,但 Record不是。 因為 Record並不需要輸入類型來拷貝屬性,所以它不屬於同態:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
// 非同態類型本質上會創建新的屬性,因此它們不會從它處拷貝屬性修飾符。
// 由映射類型進行推斷
// 現在你了解了如何包裝一個類型的屬性,那么接下來就是如何拆包。 其實這也非常容易:
function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}
let originalProps = unproxify(proxyProps);

注意這個拆包推斷只適用於同態的映射類型。 如果映射類型不是同態的,那么需要給拆包函數一個明確的類型參數。

預定義的有條件類型

  • TypeScript 2.8在lib.d.ts里增加了一些預定義的有條件類型:
  • Exclude<T, U> -- 從T中剔除可以賦值給U的類型。
  • Extract<T, U> -- 提取T中可以賦值給U的類型。
  • NonNullable<T> -- 從T中剔除null和undefined。
  • ReturnType<T> -- 獲取函數返回值類型。
  • InstanceType<T> -- 獲取構造函數類型的實例類型。

例如:

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void
type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]
function f1(s: string) {
    return { a: 1, b: s };
}
class C {
    x = 0;
    y = 0;
}
type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error
type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

注意:Exclude類型是建議的Diff類型的一種實現。我們使用Exclude這個名字是為了避免破壞已經定義了Diff的代碼,並且我們感覺這個名字能更好地表達類型的語義。我們沒有增加Omit<T, K>類型,因為它可以很容易的用Pick<T, Exclude<keyof T, K>>來表示。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM