TypeScript 快速入門

jserTang發表於2019-01-10

[Toc]

typescript 的產生意義

  • javascript 的特性:
    • javascript 是一門弱型別的,動態型別檢查的語言。這兩個特性在帶來靈活與便捷的同時,也天生有一些缺陷。
    • 弱型別
      弱型別指的是,早宣告一個變數的時候,不需要指定型別,在給這個變數重新賦值的時候也不需要是固定的型別。不像 java 等強型別的語言,宣告一個變數的時候需要指定型別,且不能被賦予非指定型別的值。
    • 動態型別檢查
      靜態型別語言會在編譯階段就會丟擲型別錯誤,避免了線上上出型別問題。而js 的型別檢查不是在編譯階段進行,而是在執行階段進行的。當產生型別檢查的錯誤的時候,只有在執行的的時候才會顯現出來。 例如:下面有一個分割字串的函式,但是如果不小心在呼叫的時候傳入其他型別的資料作為引數的話,在書寫和編譯的時候不會丟擲錯誤,但是會在執行時丟擲錯誤。這種錯誤往往會引起整個程式的崩潰。下面的程式碼因為不小心給函式 someFunc 傳了個字串型別的引數,所以執行時報錯了......
      const someFunc = (string) => {
          return string.split('');
      } 
      someFunc(3);
      // Uncaught SyntaxError: string.split is not a function
      複製程式碼
  1. typescript的作用:
  • 編譯時的型別檢查,在編譯時對程式碼錯誤進行檢查並丟擲錯誤
  • ide的增強,程式碼智慧提示,interface的提示等
  • 提升程式碼可讀性,穩定性、可重構性。

型別

1. 基礎資料型別

  • 1.1 boolean
    const isTrue: bollean: true;
    複製程式碼
  • 1.2 number
    let num: number = 2333;
    num = 'abc';  // Error: 不能將型別“"abc"”分配給型別“number”
    複製程式碼
  • 1.3 string
    let str: string = '嘿嘿嘿';
    str = 0;  // Error: 不能將型別“0”分配給型別“string”。
    複製程式碼
  • 1.4 null 和 undefined

在非嚴格空檢查模式下,null 和 undefined 是所有型別的子型別,可以作為任何型別變數的值;
在嚴格空檢查模式(strictNullChecks)下,其是獨立的型別。

非嚴格空檢查模式下:以下三種情況都不會報錯:
嚴格空檢查模式下:以下三種情況都會報錯:

let str: string = undefined;

let obj: object = undefined;

let num: number = 2;
num = null;
複製程式碼
  • 1.5 void

void 表示空型別,void 型別只能賦值為 null || undefined。也可以在函式中表示沒有返回值。

let v: void = null;

let func = (): void => {
    alert('沒有返回值');
}
複製程式碼
  • 1.6 never

never 表示其有無法達到的重點,never 是任何型別的子型別,但沒有任何型別是 never 的子型別;

const error = (message: string): never => {
    throw new Error(message);
}
// 雖然這個函式規定必須有 string 型別的返回值,但是由於 never 是任何型別的子型別,所以這裡不會報錯
const error = (message: string): string => {
    throw new Error(message);
}
複製程式碼
  • 1.7 any

any 表示該值是任意型別,編輯時將跳過對他的型別檢查。應在程式碼中儘量避免 any 型別的出現,因為這會失去 ts 的大部分作用 -- 型別檢查。其使用的場景在於接受的資料時動態不確定的型別,或者用來在第三方庫的 module.d.ts 宣告檔案檔案裡跳過對第三方庫的編譯。

// 例如在 React 的宣告檔案裡,因為不確定傳入的nextProps 與 nextContext 是什麼型別,所以使用了any
componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void;

複製程式碼
  • 1.8 object

object表示非原始型別,也就是除number,string,boolean,symbol,null或undefined之外的型別。其意義在於,在不知道傳入的物件長什麼樣子的情況下,更容易表示Obiect的api,例如hasOwnProperty

const func = (arg: object): void => {
    console.log(arg.hasOwnProperty('name')); // => true
}
func({name: 'liuneng'})
複製程式碼
  • 1.9 陣列與元組

    1. 陣列

    有兩種定義陣列型別的方式,一種是直接在型別後面加上[], 表示元素為該型別的陣列

    let arr: number[] = [];
    arr.push(1); 
    arr.push('2');  // Error: 型別“"2"”的引數不能賦給型別“number”的引數
    複製程式碼

    第二種是使用陣列泛型, 這種方式可以在不想在外邊宣告型別時候使用

    let list: Array<1 | 2> = [];
    list.push(3);  // Error: 型別“3”的引數不能賦給型別“1 | 2”的引數
    複製程式碼
    1. 元組用來表示已知元素數量與型別的陣列,在賦值時內部元素的型別必須一一對應,訪問時也會得到正確型別。當給元組新增元素或者訪問未知索引元素的時候,會使用他們的聯合型別
    let tuple: [string, number];
    tuple = [1, 'a'];  // Error: 不能將型別“[number, string]”分配給型別“[string, number]”
    tuple = ['a', 1];
    
    tuple.push(true);  // 型別"true"的引數不能賦給型別“string | number”的引數。
    複製程式碼
  • 2.0 型別斷言

其表示在不確定該變數型別時,指定其型別,表示明確知道他的型別,不用去檢查了

// 雖然 param 是any,但“我”保證傳入的一定是個 string 型別的引數
const func = (param: any) => {
    return (param as string).length;
};
複製程式碼

2.列舉 (enum)

使用列舉型別可以為一組數值賦予有意義的名字

  • 例如說,現在有一個介面用來過濾一個列表,其接受一個引數,0表示不過濾,1表示過濾男性, 2表示過濾女性
const param = {
    filterType: 0,
};

fetch('/getfilterList', param)
  .then((res: any[]) => {
    console.log(res);
  });
複製程式碼

上面的這行程式碼,用眼睛看根本不知道請求的是什麼型別的列表,只能通過註釋 與 文件來判斷它的意義

enum filterMap {
    All = 0,
    Men = 1,
    Women = 2,
}
const param = {
    filterType: filterMap.Men,
};

fetch('/getfilterList', param)
  .then((res: any[]) => {
    console.log(res);
  });
複製程式碼

上面這段程式碼,用列舉列出了所有過濾條件的選項,使用時直接像使用物件一樣列舉,從語義上很容易理解這段程式碼想要獲取的是男性列表,程式碼即是文件。尤其是當做常量使用更加統一與方便理解。

  • enum 的值
    宣告 enum 型別的時候,可以指定 value 也可以不指定 value。
    不指定 value 的話他會從零後續依次遞增 1。
    當通過 value 訪問 key 的時候,如果有相同的 value,取最後一個
enum AbcMap {
    A,
    B = 1,
    C,
    D = 2,
    E = 2,
}
console.log(AbcMap.A);  // => 0   因為後面的 B 是1,所以自動 -1
console.log(AbcMap.C);  // => 2   因為前面的 B 是1,所以自動 +1
console.log(AbcMap.[2]); // => E 有三個2,取最後一個的 key
複製程式碼

3.介面 (interface)

interface 是對物件形狀的描述,其規定這個型別的物件應該長什麼樣子,編譯的時候回去檢查以他為描述的物件符不符合其結構

interface IPerson {
    name: string;
    readonly isMan: boolean;  // 只讀屬性,建立後不可以改寫
    age?: number;   // 可選屬性,實現的時候可以沒有這個屬性
}

const xiaohong: IPerson = {
    name: '小紅',
    isMan: false,
};

xiaohong.isMan = true;  // Error: isMan 是常數或只讀屬性。

xiaohong.love = '周杰倫'; // Error: “love”不在型別“IPerson”中。
複製程式碼

上面給小紅新增 love 屬性的時候報錯了,因為 IPerson 中沒有規定這個屬性

但是有時候我們不確定在 interface 外有沒有別的屬性,這時候可以使用索引簽名。但是此時已確定的型別必須是他的子型別

interface IPerson {
    [key: string]: string;
}

const xiaoming: IPerson = {
    name: '小紅',
    love: '周杰倫'
};
複製程式碼

4.函式

ts 可以給函式的引數 與 返回值指定型別。使用時候不能使用多餘引數

  • 函式宣告:

現在定義一個加法的函式表示式

const sum: (x: number, y: number) => number = (x: number, y: number): number => x + y;
sum(1, 2); // => 3
複製程式碼

上面的程式碼看起來可能有點不好理解,左邊是給 sum 定義型別,右半部分是 一個具體函式,這是 ts 函式完整的寫法。通過 ts 型別推論的特性,可以把左半部分省略掉;也可以給變數定義型別而省略右邊

const sum = (x: number, y: number): number =>  x + y;
複製程式碼

上面的程式碼看起來就比較好理解了,但是如果我們有一個乘法的方法,還有減法的方法等等等等,其輸入型別和輸出的型別都是 number,這個時候如果感覺在每個方法上都去定義引數與返回值的型別會覺得有點麻煩。此時,可以單獨抽出一種函式型別,在函式表示式中使用。

type INumFunc = (x: number, y: number) => number ;

const sum: INumFunc = (x, y) =>  x + y;
const sub: INumFunc = (x, y) =>  x - y;
const multip: INumFunc = (x, y) =>  x * y;
複製程式碼

上面的程式碼定義了一個函式型別,要求輸入輸出都為 number;此時 ts 會自動給右邊的函式體確定函式型別。如果右邊函式體與左邊型別宣告不一致就會報錯。

  • 引數:
    ts 當不確定函式的引數的時候,可以定義可選引數,其餘 interface 的可選屬性使用方法類似,是一個問號。也可以使用預設引數,其與 ES6 的預設引數一致
// 可選引數
const sub = (x: number, y: number = 5, y?: number): number =>  {
    if (y) {
        return x - y - z;
    } else {
        return 0;
    }
};
sub(10, 1, 1)  // -> 8
sub(10, 1)     // -> 0


// 預設引數
const sum = (x: number, y: number = 5): number =>  x + y;
sum(1, 2);      // -> 3
sum(1);         // -> 6
sum(1, null);   // -> 6
複製程式碼

js 裡有 arguments 的存在,所以我們可以給一個函式傳任意個引數。在 ts 裡,不確定引數的個數的話,可以使用剩餘引數,將多出的引數放入一個陣列, 其和 ES6 的剩餘引數使用方法一致

const sum = (x: number, ...y: number[]): number =>  {
    console.log(y);
    let sum = x;
    if (y && y.length > 0) {
        for (let index = 0; index < y.length; index++) {
            sum = sum + y[index];
        }
    }
    return sub;
};

sum(1, 2, 3);  // res -> 6  ,  log -> [2, 3]
複製程式碼

5. 類(class)

ts 的類 與 ES6 中的類大體相同,不過 class 中的屬性可以新增修飾符

static 靜態屬性,其是這個類的屬性,而不是例項的屬性 public: 訪問該成員的時候沒有限制;
protected: 在派生類中可以訪問該屬性,但是不能再外部訪問;
private: 私有成員,只能自己訪問
readonly: 只讀屬性
abstract: 用於修飾抽象類或屬性,必須在派生類中方實現它,自己不能實現。

class Person {
    static desc() {
        console.log('It's a class of "person");
    }

    protected name: string;
    private age: number = '8';
    readonly sex: string = 'boy';
    
    constructor (theName: string) {
        this.name = theName;
    }
    
    public like() {
        console.log('footbal');
    }
    
    abstract eat(): void;  // 必須在派生類中實現它
}

class kids extends Person {
    constructor(name) {
        super(name);
    }
    sayName() {
        console.log(this.name);
    }
    
    eat() {
        console.log('麵包');
    }
}

const xiaohong = new kids('小紅');

Person.desc();   // 靜態成員直接使用 class 訪問,不用例項

xiaohong.like(); // -> 'footbal'  public 屬性訪問沒限制

console.log(xiaohong.name);  // Error: 小紅是 protected 屬性,只能在基類與派生類裡面訪問
xiaohong.sayName();  // -> '小紅' 小紅的內部方法裡可以訪問 protected 屬性

console.log(xiaohong.age) // age 是 私有屬性,不能在外部訪問

console.log(xiaohong.sex); // -> boy
xiaohong.sex = 'girl'; // Error: sex 是隻讀屬性,不能修改
複製程式碼

6. 型別推論

TypeScript 會在沒有明確的指定型別的時候推測出一個型別,這就是型別推論。

  • 賦值
let str = 'string';
str = 1;
// Error: Type 'number' is not assignable to type 'string'.
// str 在宣告的時候並沒有指定型別,但是 ts 自動推斷為 string, 所以在給它賦值為 number 的時候報錯了

let someVar;
someVar = 'string';
someVar = 3;
// 如果在宣告一個變數的時候並沒有給它賦值,ts 自動給它推斷為 any 型別,所以這裡跳過了型別檢查,沒有報錯。
複製程式碼
  • 函式
const sum = (x: number, y: number) => x + y;
sum(1, 2);
// 上面函式,沒有並沒有給其指定 return 的型別,但這是被允許的,因為 ts 可以自動推斷出其返回值的型別。
複製程式碼
  • 物件字面量
const obj = {
    a: 1,
    b: 2,
};
obj.a = 'str';  // Error: 不能將型別“"str"”分配給型別“number”
// 雖然 obj 在宣告的時候並沒有指定型別,但是 ts 自動將其推斷為 {a: number, b: number} 所以報錯

// 解構也是一樣的
let { a } = obj;
a = 'str';     // Error: 不能將型別“"str"”分配給型別“number”
複製程式碼
  • 陣列

下面的程式碼將 arr 推斷為了 Array<string | number>

const arr = ['a', 'b', 1];
arr[0] = true;  // Error: 不能將型別“true”分配給型別“string | number”
複製程式碼
  • 型別保護

    ts 甚至能根據某些程式碼特徵進行推斷出正確的型別範圍。

    • typeof

    下面的 if 程式碼塊中,param 被推斷為型別 string

      const func = (arg: number | string) => {
          if (typeof arg === 'string') {
              console.log(arg.split(''));  // OK
          }
          console.log(arg.split(''));  // Error: 型別“number”上不存在屬性“split”。
      }
    複製程式碼
    • instanceof

    下面的程式碼可以根據 instanceof 推斷出其引數型別,甚至可以自動推斷出 else 程式碼塊中的型別

      class A {
          public name: string = 'hehe';
      }
    
      class B {
          public age: number = 8;
      }
    
      const func = (arg: A | B) => {
          if (arg instanceof A) {
              console.log(arg.name);  // OK
              console.log(arg.age);  // Error: 型別“A”上不存在屬性“age”。
          } else {
              console.log(arg.name); // Error: 型別“B”上不存在屬性“name”。
              console.log(arg.age);  // OK
          }
      }
    複製程式碼

7. 泛型

有時候,當時用一個元件的時候,並不能確定其資料型別是什麼樣子,或者說為了達到複用元件的目的,可以使用泛型來建立可重用的元件。

例如,現在需要一個函式,其要求可以輸出任意型別的引數,但是輸入與輸出必須是同一型別。如果不使用泛型的話,只能使用聯合型別,或者 any 來實現。使用泛型可以這樣做:

function identity<T>(arg: T): T {
    return arg;
}
identity<string>('str'); // -> 'str'
複製程式碼
  • 泛型既可以代表未來使用時的型別,也可以作為型別的一部分
function objToArr<T>(arg: T): T[] {
    return [arg];
}

objToArr({a: 1});  // -> [{a: 1}]
複製程式碼

上面的程式碼表示輸入 T 型別的引數時,返回一個 T 型別成員的陣列

// 建立一個介面,其屬性 list 的型別在使用前並不確定
interface IData<T> {
    list: T[];
    status: number;
}

const numItemData: IData<number> = {
    list: [1, 2, 3],
    status: 1,
};

const strItemData: IData<number> = {
    list: ['a', 'b', 'c'],  // Error: 不能將型別“string[]”分配給型別“number[]”。
    status: 1,
};
複製程式碼

上面的例子建立了介面 IData,其在使用的時候,傳入型別約束, 這樣可以最大程度的複用 IData 介面。因為 strItemData 的賦值與泛型傳入的型別不一致所以報錯

  • 配合 fetch 使用。 大部分情況下,我們在處理 feth 請求的時候,與後端約定,後端返回的資料格式是固定的,只不過 data 部分可能並不確定。
// 我們與後端約定, response 的格式如下,但 data 部分依具體使用場景而定, 泛型可以給個預設值 - any
interface IResponse<T = any> {
    status: number;
    message: string;
    data: T;
}
複製程式碼
// 我們封裝了一個 fetch API,裡面對請求進行了處理,例如header、toaster
import fetchData from 'XXX/fetchData';

// 引入上面定義的通用的 response 介面
import { IResponse } from 'XXX/response';

export const getUser<T> = (param: IInput): Promise<IResponse<T>> => {
    return fetchData('xxx/getData').then((res: IResponse<T>) => {
        return res;
    });
};
複製程式碼

使用的時候:

import getUser from 'XXX/getUser';

// 定義 response 中 data 的型別
interface IData {
    name: string;
    age: number;
}

// 將 data 的型別約束傳入泛型
const userInfo = getUser<IData>();

userInfo.data.name = '小剛';  // Right
userInfo.data.name = 666;   // Error
// ts 推斷出 data.name 是 string 型別,所以在賦值為 666 的時候報錯了
複製程式碼
  • 泛型類
class Person<TName, TAge> {
    name: TName;
    age: TAge;
}

let xiaoming = new Person<string, number>();
xiaoming.name = '小明';  // Right
xiaoming.age = '8';  // Error: [ts] 不能將型別“8”分配給型別“number”。
複製程式碼

上面程式碼因為在建立 xiaoming 的時候規定了 age 型別必須為 number,所以報錯了

  • 泛型約束 可以對泛型的結構進行約束
interface IData {
    a: number;
}

function objToArr<T extends IData>(arg: T): T[] {
    console.log(arg.a);
    return [arg];
}

objToArr({a: 1, b: 2});  // -> [{a: 1, b: 2}]
objToArr({b: 2});  // Error: 型別“{ b: number; }”的引數不能賦給型別“IData”的引數。
複製程式碼
  • 內建物件

TS 為我們的 javascript 的內建物件提供了型別,並且在使用內建物件的時候自動為我們進行型別檢測 例如:

let body: HTMLElement = document.body;

let div: HTMLDivElement = document.createElement('div');

document.addEventListener('click', function(e: MouseEvent) {
    console.log('MouseEvent');
});

Math.round('3.3');  // 型別“"3.3"”的引數不能賦給型別“number”的引數
// 因為 Math 物件 round 需要接受一個 number 型別的引數所以報錯了,
// 下面是TS核心庫定義檔案中對 Math 物件的定義
/**
interface Math {
    pow(x: number, y: number): number;
    random(): number;
    round(x: number): number;
    sin(x: number): number;
    // ......
}
declare const Math: Math;
**/
複製程式碼
  • 不建議使用的內建型別

TS 也定義了 Number,String,Boolean, Object, 但是並不推薦區用這些型別,而是應該使用 number, string, bollean, obiect

let str: String;  // Don't use 'String' as a type. Avoid using the `String` type. Did you mean `string`
複製程式碼

型別別名

可以給型別起個名字

  • 可以將字面量作為一個型別
type str = 'a';
type num = 1;
const ab: str = 'ab'; // Error: 不能將型別“"ab"”分配給型別“"a"”。
const someNum: num = 2; // Error: 不能將型別“2”分配給型別“1”。
複製程式碼
  • 可以像 interface 一樣使用

雖然使用方式類似,但是型別別名並不能被繼承、匯出等操作。只能作為

type Person = {
    name: string;
    age: number;
};

const xiaoming: Person = {
    name: '小明',
    age: 18,
};
複製程式碼
  • 型別別名配合 泛型
type Person<T> = {
    name: string;
    like: <T>[];
};

const xiaohong: Person<string> = {
    name: '小紅',
    like: ['dance', 'football'],
};
複製程式碼
  • 利用型別別名來進行遍歷物件

當我們想要比那裡一個物件的時候,需要指定每一項元素的 key 的索引簽名,否則會報錯,比如像下面這樣

const obj = {a: 1, b: 2};
for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(obj[key]);
    }
}
// Error: 元素隱式具有 "any" 型別,因為型別“{ a: number; b: number; }” 沒有索引簽名。
複製程式碼

可以使用 型別別名 + 索引型別來避免該問題

const obj = {a: 1, b: 2};
type ObjKey = keyof typeof obj; // => 'a' | 'b'

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(obj[key as ObjKey]);
    }
}
複製程式碼

宣告檔案

宣告檔案是對某個庫的環境宣告,檔案內對庫向外暴露的 API 進行型別註解,使其在使用的時候可以享受到 TS 型別系統帶來的檢查

  • 宣告變數與函式
// 宣告變數
declare var foo: number;

// 宣告函式
declare function add(x: number, y: number): number;
複製程式碼
  • 宣告一個 interface
// 直接將 interface 匯出就行
複製程式碼
  • 宣告一個物件

// 1. 使用名稱空間的方式
declare namespace person {
    let name: string;
    let age: number;
}

// 2. 使用 interface
interface IPerson {
    name: string;
    age: number;
}
declare const person: IPerson;
複製程式碼
  • 宣告一個類
declare class Person {
    constructor(name) {
        this.name = name;
    }
    sayHi() {
        console.log(`I'm ${this.name}`);
    }
}

複製程式碼
  • 在安裝 TypeScript 的時候,會自動安裝 lib.d.ts 等宣告檔案。其內部包含了 JavaScript 內建物件及 DOM 中存在各種常見的環境宣告。例如: es5.d.ts

相關文章