循序漸進理解TypeScript型別模式

christolan發表於2019-04-26

認識TypeScript

TypeScript是微軟推出的一個強型別JavaScript超集,具備了比JavaScript更嚴禁的型別定義,備受程式設計師喜愛的編輯器VSCode就是使用TypeScript寫的,後面簡稱ts。

在使用js的時候,一個變數可以被任意賦值,而且不管你怎麼寫總是不會報錯,這就意味著很多時候錯誤被埋藏在看似正常的程式碼中。不同於js的直接執行,ts程式碼必須通過編譯成js來執行,很多錯誤會在編譯的時候就報錯,大大降低了發現低階錯誤的成本。

尤其在寫大型專案的時候,ts的優勢更加明顯。例如我們寫了一個元件,元件使用了一個傳過來的物件作為props,突然我們需要給元件增加一個功能,需要多傳入一個資料,因此我們需要給props物件新增一個屬性。如果使用js,這種情況下確定哪些地方需要做對應的修改是一件很頭疼的事情,而ts則很容易,我們修改一下props物件的介面型別,所有需要修改的地方就都會報錯,依次修改報錯的地方就可以了。

網上各類ts的教程很多,但是很少有站在一個不懂ts的立場上來寫的,這篇文章的目的,就是帶領那些熟悉js的人,一步步理解ts新增的型別模式,把一個陡峭的山坡,變成一級級的臺階,幫助想用ts的人快速入門。

基礎型別

在js中,很多的東西都是以物件的形式存在的,除了最基礎的5種資料型別:

  • number
  • string
  • boolean
  • null
  • undefined

這五種最基本的資料型別的定義是很直白的,就是冒號加名字,如下所示:

let num: number = 0;
let str: string = "";
let boo: boolean = false;
let foo: null = null;
let bar: undefined = undefined;
複製程式碼

從上面的例子可以看出,ts和js的區別,就是在變數後面緊跟了一個冒號,冒號後面寫下了變數的型別。記住這一點,下面的內容就能理解了,如果還沒有理解,就需要反覆體會一下,理解了再繼續往下。

void

除了上面的這些型別以外,ts還增加了一種特殊的型別:void

let a: void = null;
複製程式碼

一個void型別的變數只能被賦值為null或者undefined,它通常用在函式中,指定函式的輸出為void意味著這個函式沒有返回值。

any

除了上面介紹的6大基礎型別之外,還一個萬能型別:any

let a: any = "hello,world!"
複製程式碼

萬能型別就是js本身的樣子,一個變數可以被定義為任何型別,也是使用ts的時候應該儘量避免使用的型別,使用過多的any就讓ts退化成了js,失去了使用ts的意義。

聯合型別

ts支援聯合型別,意味著一個變數可以有不止一個型別,如下所示:

let name: string | number = 'mike';
name = 18; // 允許賦值兩種型別的資料
複製程式碼

聯合型別賦予了ts變數一定的自由,但是還不如js那麼自由。對聯合型別的屬性和方法的訪問是受限的,在給name賦值之前只能訪問公共的屬性與方法,在賦值以後則只能訪問由被賦值的型別擁有的屬性與方法。

聯合型別是一種處於基礎型別與any型別之間的中間態。

物件的型別:interface

有了基礎的型別,還需要一種方法來定義物件的型別,因為物件就是一些基礎型別拼湊起來的,所以缺少的只是一個封裝的方法,因此ts引入了interface關鍵字。

宣告一個interface:

interface person {
    name: string;
    age: number;
}
複製程式碼

然後就可以使用這個interface來限制一個物件的型別:

let tom: person = {
    name: 'tom',
    age: 12
}
複製程式碼

有了interface我們就可以把一堆基礎型別封裝成一個物件的型別。需要注意的是宣告介面的時候用的是;分號,而物件中用的是,逗號。

可選型別

在物件中除了必須的屬性與方法以外,可能還有些屬性或方法不是必須的,於是就誕生了可選型別。在interface中放一個可選型別的方式如下所示:

interface person {
    name: string;
    age?: number; // 可以沒有這個屬性
}
複製程式碼

這樣我們在申明一個型別為person的物件時就可以不給它age屬性,讓這個物件可以變小

任意型別

一個物件中除了必須的型別和可有可無的型別外,我們還希望能後期增加型別,於是就誕生了任意型別。給interface新增一個任意屬性的方式如下:

interface person {
    name: string;
    [propName: string]: string;
}
複製程式碼

這樣我們就可以給一個person型別的物件新增值為string的屬性,讓這個物件可以變大

這裡需要注意一個很關鍵的問題:當介面中存在任意屬性時,其他的所有屬性都必須是任意屬性的子集!,如下所示的介面定義就是錯誤的:

interface person {
    name: string;
    age?: number;
    [propName: string]: string;
}
複製程式碼

上面的可選屬性age的變數型別是number,不是任意屬性string的子集,所以編譯的時候就會報錯。

只讀型別

我們可以給interface宣告一個只讀屬性或方法,只能在初始化變數的時候賦值而不允許後續的修改,只要在屬性或方法前面加上一個readonly關鍵詞,如下所示:

interface person {
    readonly id: number;
    name: string;
}
複製程式碼

特殊物件的型別

有了一般物件的型別,還剩下一些特殊的物件的型別:陣列與函式。

陣列

陣列的型別有三種定義方式。

陣列宣告

第一種是最簡單也最直觀的定義方式,直接在元素型別後面加上一對[],如下所示:

let arr: number[];
複製程式碼

這就定義了arr為一個number陣列。

介面定義

既然陣列是一種特殊的物件,自然也就可以使用物件的型別定義方式:interface,具體如下所示:

interface NumberArray {
    [index: number]: number;
}
let arr: NumberArray;
複製程式碼

上面定義的NumberArray就是一種鍵為數字,值也為數字的物件,也就是數字陣列。

泛型定義

除了上面兩種定義方式,還可以使用泛型來定義一個陣列的型別,這一點在後續泛型的章節中討論。

元組

弱型別的js允許在一個陣列中儲存不同型別的資料,元組就是一種混雜型別的陣列:

let mike: [string, number] = ["boy", 12];
複製程式碼

函式

函式的宣告方式有很多種,ES6引入的箭頭函式得到了廣泛的使用,在此之前函式的宣告主要有兩種方法:函式宣告與函式表示式。這兩種寫法的型別定義方式是不一樣的。

函式宣告

函式宣告使用function關鍵字來宣告一個函式:

function sum(x, y) {
    return x + y;
}
複製程式碼

函式宣告的ts型別定義是比較簡單的:

function sum(x: number, y: number): number {
    return x + y;
}
複製程式碼

比較直觀也很好理解,只需要記住函式的輸出在形參的括號後面接冒號定義,別的都很直白。

函式表示式

函式表示式使用let或const(實際上幾乎都是const)來宣告一個匿名函式並賦值給一個變數:

const sum = function(x, y) {
    return x + y;
}
複製程式碼

函式表示式的型別定義是極其複雜的,如下所示:

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

不僅需要在匿名函式這裡宣告輸入輸出的變數型別,還需要宣告儲存這個函式的指標的型別,而且這個型別寫起來極其複雜,使用了與箭頭函式一樣的=>操作符,這種寫法讓人討厭的地方在於感覺需要重複寫一遍型別定義。

介面定義

函式也是特殊物件,可以使用介面來定義輸入輸出型別,這樣就意味著,我們會指定一個變數,它只能被賦值某個樣子的函式:

interface sum {
    (x: number, y: number): number;
}

let mySum: sum;
複製程式碼

上面的介面定義與函式宣告使用的定義很相像,都是一個圓括號包住輸入並分別定義型別,然後在括號外使用一個冒號定義輸出的型別。

基本型別操作

型別斷言

在聯合型別中,還沒被賦值的變數只能使用型別公共的屬性與方法,而使用型別斷言則可以使用聯合型別中某個型別的屬性與方法:

let something: string | number;
something.length; // 會報錯
(<string>something).length; // 不會報錯,注意型別與斷言要用括號包起來當成一個變數使用
複製程式碼

型別斷言的使用方法就是在變數前面使用尖括號斷言一個型別。如果沒有型別斷言,因為something的型別為string與number的聯合型別,所以不能訪問只有string才有的length屬性,但是通過型別斷言就可以訪問了。

型別斷言的作用就是明確告訴ts這個變數的型別。型別斷言還有另外一種書寫形式:

<string>something
===
something as string
複製程式碼

關於型別斷言,需要記住它的通項:

  • <型別>變數:在變數前面加尖括號型別是斷言
  • 變數 as 型別:在變數後面加as關鍵字接型別是斷言

型別斷言很關鍵的一點就是不要與泛型搞混。

型別別名

型別別名使用type操作符。

型別別名的作用就是用一個簡短的變數來儲存一長傳的型別定義。型別與型別之間也是可以使用運算子執行邏輯運算的,比如聯合型別的|操作符。下面是一個使用型別別名的例子:

type numstr = number | string;
let a: numstr = 2;
a = "tom";
複製程式碼

在函式表示式的型別中,我們使用了一串很複雜的東西來表示一個函式變數的型別:

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

我們可以使用一個別名來簡化它:

type sumFun = (x: number, y: number) => number;
let sum: sumFun;
複製程式碼

型別別名可以理解成宣告一個型別變數的方式,使用一個變數來儲存一個具體的型別。不要把型別別名type與介面interface搞混。

字串字面量型別

字串字面量也使用type操作符。

字串字面量的作用就是把一個變數的型別限定為某幾個特定的字串,一個型別為string的變數可以儲存任何字串,但是字串字面量型別只能儲存特定的字串中的一個。

type EventNames = 'click' | 'scroll' | 'mousemove';
let event: EventNames = 'click'
let event: EventNames = 'click2' // 會報錯
複製程式碼

型別總結

我們已經知道了最基本的7個型別:

  • number
  • string
  • boolean
  • null
  • undefined
  • void
  • any

以及由他們組成的一些高階型別:

  • interface定義的介面
  • 陣列
  • 函式
  • type定義的別名與字串字面量

高階型別操作:泛型

我們可能會遇到一個問題:

  • 一個函式,我們需要輸入一些資料
  • 要將這些資料變成一個陣列輸出
  • 希望輸出陣列元素的型別與輸入資料的型別相同
  • 而具體是什麼型別要在使用這個函式的時候才能確定

如果我們給每個資料型別分別寫一個函式,那麼顯然浪費了很多資源來做重複的工作。如果把資料型別寫成any,則無法保證輸出陣列的元素與輸入型別相同。因此我們希望,能有個方式告訴這個函式,我們需要它把輸入輸出限制成什麼型別。這就是泛型的作用。

比如上面提到的例子,使用泛型來實現的程式碼如下:

function createArr<T> (x: T, y: T): T[] {
    return [x, y];
}
複製程式碼

泛型的使用相當於給了函式另外一套輸入引數,可以輸入一些型別變數,並在函式中使用。上面的T只是一個示例,它只是一個泛型的形參,可以與其他形參一樣寫成任何滿足要求的樣子。

掘金社群為本文唯一發布平臺,轉載請註明出處。

相關文章