型別即正義:TypeScript 從入門到實踐(一)

圖雀社群發表於2020-04-06

作者:一隻圖雀
倉庫:GithubGitee
圖雀社群主站(首發):圖雀社群
部落格:掘金知乎慕課
公眾號:圖雀社群
聯絡我:關注公眾號後可以加圖雀醬微信哦
原創不易,❤️點贊+評論+收藏 ❤️三連,鼓勵作者寫出更好的教程。

源起

JavaScript 已經佔領了世界上的每一個角落,能訪問網頁的地方,基本上就有 JavaScript 在運作,然而 JavaScript 因為其動態、弱型別、解釋型語言的特性、出錯的呼叫棧隱蔽,使得開發者不僅在除錯錯誤上花費大把時間,在團隊協作開發時理解隊友編寫程式碼也極其困難。TypeScript 的出現極大的解決了上面的問題,TypeScript -- 一個 JavaScript 的超集,它作為一門編譯型語言,提供了對型別系統和最新 ES 語法的支援,使得我們可以在享受使用 ES 最新語法的編寫程式碼的同時,還能在寫程式碼的過程中就規避很多潛在的語法、語義錯誤;並且其提供的型別系統使得我們可以在團隊協作編寫程式碼時可以很容易的瞭解隊友程式碼的含義:輸入和輸出,大大提高了團隊協作編寫大型業務應用的效率。在現代 JavaScript 世界中,已經有很多大型庫在使用 TypeScript 重構,包括前端三大框架:React、Vue、Angular,還有知名的元件庫 antd,material,在很多公司內部的大型業務應用也在用 TypeScript 開發甚至重寫現有的應用,所以如果你想編寫大型業務應用或庫,或者想寫出更利於團隊協作的程式碼,那麼 TypeScript 有十足的理由值得你學習!本文是 TypeScript 系列教程的第一篇,主要通過使用 antd 元件庫實戰演練一個 TypeScript 版本 React TodoList 應用來講解 TypeScript 的語法,使得你能在學會語法的同時還能完成一個實際可執行的專案。

本文所涉及的原始碼都放在了 Github  或者 Gitee 上,如果您覺得我們寫得還不錯,希望您能給❤️這篇文章點贊+Github** 或 Gitee 倉庫加星❤**️哦~

此教程屬於 React 前端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵我們繼續創作出更好的教程,持續更新中~

程式碼準備


我們接下來要講解的整個 **型別即正義:TypeScript 從入門到實踐 **系列是基於一個實戰專案的,這個實戰專案會貫穿整個系列教程的講解週期,所以我們要儘可能全且精煉的講解 TypeScript 語法知識的同時,還我們需要一個恰到好處的實戰專案,因為準備專案程式碼的過程不是系列教程講解的主線,所以如果你有興趣學習如何搭建 TypeScript React 的開發環境,那麼可以學習一下我們的序言教程:

型別即正義:TypeScript 從入門到實踐(序章)
**
如果你已經對 TypeScript 如何搭建 React 開發環境,配置 Ant Design 元件庫等很熟悉,或者不太感興趣,那麼你也可以直接克隆我們為你準備好的程式碼:

如果你偏愛 碼雲,那麼你可以執行如下命令來獲取初始程式碼:

git clone -b initial-code https://gitee.com/tuture/typescript-tea.git
cd typescript-tea && npm install && npm start
複製程式碼

如果你偏愛 Github,那麼你可以執行如下命令來獲取初始程式碼:

git clone -b initial-code git@github.com:tuture-dev/typescript-tea.git
cd typescript-tea && npm install && npm start
複製程式碼


通過上面的命令克隆初始程式碼之後,然後把專案跑起來,你應該可以看到如下效果:

image.png


Boom!!!一個暗黑模式的 TodoList,心動了嘛?不管心不心動,你都可以愉快的開始接下來的 TypeScript 學習了✌️。

TypeScript 初探


正式 TS 時間☕️,TS 是一門靜態程式語言,它是 JavaScript 的超集。首先我們先來解釋一下什麼是程式語言,然後我們再來引出 TypeScript 是什麼。

程式語言是什麼?


那麼什麼是程式語言了?程式語言是用來定義計算機程式的形式語言。它是一種被標準化的交流技巧,用來向計算機發出指令。

我們拿 JS 來舉例,一門標準的程式語言一般包含如下幾個部分:

  • 資料結構:如原始資料型別 string/number/void 等,非原始資料型別 array/object/enum 等
  • 控制結構:如 if/else 、 switch 、while、for 迴圈等
  • 組織結構:如 函式、類
  • 特性:如 JS 的原型鏈
  • 常用的 API:如 isNaN 判斷是不是非數字,toFixed 將小數進行四捨五入操作
  • 執行環境:如 瀏覽器端的 JavaScript、伺服器端的 Node


其中前五種又稱為語言核心,也就是我們常常喊的 ECMAScript 2015,或者 ES6;最後一個執行環境在瀏覽器端結合 BOM/DOM 即成為 JavaScript,在伺服器端結合一些檔案/網路的操作即成為 Node。

TypeScript 是什麼?


而 TS,作為 JavaScript 的超集,包含著兩類屬性:

  • 屬於 JavaScript 端的程式語言特性,使得我們可以執行各種 JavaScript 相關的操作:變數宣告、編寫 if/else 控制流、使用迴圈處理重複任務、使用函式執行特定的任務塊、使用類來組織和複用程式碼和模擬真實世界的操作等,還有新特性比如:裝飾器、Iterator、Generator 這些。這類特性在此篇文章中,我們預設你已經很清楚了,不會做過多的講解。
  • 屬於 TypeScript 端獨有的特性:型別,它也具有一套程式語言的特性,比如標誌一個變數是 string 型別,一個函式的引數有三個,它們的型別分別是 string/number/boolean,返回型別為 never等,這是基礎型別,我們甚至可以基於型別進行程式設計,使用型別版本的控制、組織結構來完成高階型別的編寫,進而將型別附著在 JavaScript 對應的程式語言特性上,將 JS 靜態化,使得我們可以在編譯期間就能發現型別上的錯誤,這一特性是我們本篇文章的重點。


好的,讀到這裡,相比很多讀者已經清楚了,其實 TS 沒什麼神祕的,主要就是設計了一套類似程式語言的型別語言,然後將這些型別附著在原 JavaScript 的語言之上,給其加上型別限制使得其靜態化,進而可以快速的在編寫時發現很多潛在的問題,幫助我們編寫錯誤率更低,更適合團隊協作的程式碼,這也是 TypeScript 適合編寫大型的業務應用的原因。

型別語言之資料結構


其中 TS 資料結構又包含原始型別、非原始型別、特殊型別和高階型別等幾類。我們將結合在 TS 型別側的定義,以及附著在 JS 上進行實戰來講解。

原始型別

TS 型別側的定義


和 JS 中的原始資料型別一樣,TS 對應著一致的型別定義,包括下面八種:

  • number
  • string
  • boolean
  • null
  • undefined
  • void
  • symbol
  • bigint

提示 其中前六種是 ES5 中就有的,symbol 從 ES6 開始引入,bigint 是 ES2020 新引進的。


上面是 TS 的原始型別,我們之前提到 TS 就是將型別附著在 JS 上,將其型別化,那麼我們來看看上面的原始型別如何附著在 JS 上,將其型別化。

附著在 JS 上的實戰


TS 通過獨特的冒號語法來將其型別側定義的型別附著在 JS 上,我們來看幾個例子:

用 JS 語言來寫圖雀社群的 Slogan,我們一般會這麼寫:

const tutureSlogan = '圖雀社群,匯聚精彩的免費實戰教程';
複製程式碼


我們可以確定,這句 Slogan 是一個 string 型別的,所以我們用對應的 TS 型別附著在其變數定義上如下:

const tutureSlogan: string = '圖雀社群,匯聚精彩的免費實戰教程';
複製程式碼


這樣我們就給原 JS 的 tutureSlogan  變數加上了型別定義,它是一個 string  型別的變數,通過這樣的操作,原 JS 變數的型別就被靜態化了,在初始化時,就不能再賦值其他的型別給這個 tutureSlogan 變數了,比如我們將 number 型別的字面量賦值給 tutureSlogan ,就會報錯:

const tutureSlogan: string = 5201314 // 報錯 Type '5201314' is not assignable to Type 'string'
複製程式碼


這就是 TS 的強大之處,當團隊編碼時事先約定好資料的型別,那麼後續編寫並呼叫這些設定好型別的變數時就會強制起約束作用,就像上面的程式碼一樣,如果給 tutureSlogan 賦值  5201314 就會報錯,其實你大可剋制一點對吧?,給 5201314 加個限制,兩邊帶上引號 '5201314' 問題就迎刃而解了,愛也可以是剋制?。

提示 有些細心的同學可能對上面的報錯資訊有點不能理解,對於報錯資訊的後半段型別 string 可能理解,因為我們給 tutureSlogan 限制了 string 型別,但是對於我們的賦值 5201314 ,它原本是一個 JS 的 number 型別的字面量,為什麼也成了 Type 了? 那是因為,TS 引擎在對語句進行編譯的時候,會對變數賦值兩端做一個型別推理,比如對賦值語句的右側 5201314 ,會將其推理成 5201314 這個型別,它是一個屬於 number 型別的一個特殊的 number 型別,可以被分配(assignable )給 number 型別的變數,這裡的 assignable 是可分配的意思,就是一個子型別可以被分配給一個父型別,比如數字 1 可以被分配給 number 數字型別,但因為 number 型別和 string 型別是衝突的,所以這裡報錯了。 這裡讀者可能會有感覺了就是,你寫的 JS 語句,加上型別定義之後,在 TS 編譯器的世界裡,一切皆型別了,它會以一種型別的視角去看待原 JS 語句,比如上面的語句,在 TS 編譯器眼裡,就是 5201314 型別和 string 型別的一個比較過程,如果比較一致,那麼好的,我 TS 編譯器今天就放你一馬,讓你逍遙快活。

小結


我們上面說到了 TS 的原始型別,一共有八個之多,並且通過其中的 string 型別來講解了如何將 TS 型別附著在原 JS 語法上以靜態化 JS 語言,剩下的 7 個原始型別的用法和 string 型別類似,我們將在之後的講解中逐漸用到其中的型別。

非原始型別

TS 型別側的定義


同樣的 JS 中的非原始資料型別一樣,TS 中也存在非原始型別,表示出了八種原始型別之外的型別,非原始型別也稱為是 object 型別。

實際上 TS 中還有幾個常見的非原始型別,例舉如下:

  • array
  • tuple
  • enum


且因為它們屬於 object 型別,所以 object 型別實際上就代表了非原始型別。在上面的三個型別以及其父型別 object 中,arrayobject 其實我們應該有點熟悉,至於 tupleenum 則是 TS 中新增的型別,JS 中正式提案中目前是沒有的。講完了型別側定義,我們馬上來實踐一下上面的 arrayenum 非原始型別。

array 型別附著實戰


其中 array 型別我們比較熟悉,但這裡有個不同就是之前我們的 JS 因為是動態語言,所以一個陣列裡面可以有各種不同的資料型別項,比如我們看如下 JS 語句:

const arr = ['1', 2, '3'];
複製程式碼


可以看到,從 TS 的角度去看這個陣列變數 arr 所包含的型別,存在字串型別 '1''3' ,以及數字型別 2 。但 TS 總的陣列型別要求陣列中的元素都是同一個型別,不允許動態變化,比如我們為上面的陣列變數 arr 宣告型別應該如下:

const arr: string[] = ['1', '2', '3'];
複製程式碼


可以看到,我們給變數 arr 宣告瞭 string[] 型別,即一個 string 型別後面跟著一個陣列標誌,表示是字串陣列型別,當宣告瞭 string[] 型別之後,我們需要把之前的陣列 2 改成字串 '2'

我們注意到 array 型別,它要求陣列中每項的型別都一樣,一般應用在陣列的長度未知的情況,用特定的型別,比如 string 型別來約束陣列的每一項。

然而從 JS 轉過來的同學大多數同學可能對這個 array 型別不適應了,我們 JS 的同學經常會遇到編寫一個陣列,其中的多項的型別不一樣,就和我們上面的 JS arr 的項一樣,既有 string 型別又有 number 型別,那這該怎麼辦了?還好!TS 的設計者也為我們考慮到了這一點,那就是我們下面要講到的 tuple  (元組)型別。

tuple 型別附著實戰


大家可能對 tuple (元組)型別很陌生了,其實元組是一種特殊的陣列型別,它主要用於這樣的場景:“一個陣列的項數已知,其中每項的型別也已知”,這句話說起來可能比較繞,我們用上面講陣列的例子來講元組:

const arr = ['1', 2, '3'];
複製程式碼


我們知道上面的陣列第一項和第三項的型別為 string 型別,第二項的型別為 number 型別,現在我們要給這個 arr 附著一個型別,使得其靜態化。

這個條件滿足我們上面說的元組的適用場景,我們通過給 arr 一個對應的元組型別,讓我們可以保持上面的寫法不變:

const arr: [string, number, string] = ['1', 2, '3'];
複製程式碼


可以看到,元組就是形如 [type1, type2, type3, ...., typen] 這樣陣列長度已知,且型別已知的情況,其中 type1typen 中所有的型別都可以不一樣。

小結


在這一小結中我們講解了一下什麼是非原始型別,然後說明了在 TS 中有四種非原始型別,其中有一種代表非原始型別 object ,然後剩下的三種屬於 object 型別。

接著我們通過實踐講解了 arraytuple 型別,對於 enum 型別和 object 型別本身,我們將留在之後的章節來講,敬請期待✌️。

特殊型別


TS 中還有幾個常用的特殊型別,它們是 anyunknownnever ,其中 never 型別一般會伴隨著和函式的型別宣告一起使用,所以我們將 never 型別的時候會提到函式的型別如何進行宣告。

接下來我們來講一講這三個型別的含義和應用。

any 型別定義與實戰


any 的字面含義是 “任何”,主要用於在編碼的時候不知道一個變數的型別,所以先給它加一個 any 型別定義,表示它可以是任何型別,一般留待後續確認此變數型別之後再將 any 改為具體的型別。

我們來看一個例子,比如我們有下面一段 TS 變數定義語句:

let demand: any;
複製程式碼


因為有時候產品給一個需求,要我們去開發一個新功能,給了設計稿,但是沒交接清楚,對於設計稿有一些內容我們想提前做,但是因為不清楚具體的型別,比如這裡的 demand ,所以我們這裡給 demand 一個 any 型別,然後繼續做其他的內容,這樣既不會出錯,也不會影響其他的開發進度。

等到產品把具體的上下文交代清楚了,誒!我們清楚了知道這個 demand 的型別了,我們就可以回過頭來給其附著一個嚴格的型別定義,比如我們知道它是 string 型別,那麼我們再返回來對其修改如下:

let demand: string;
複製程式碼


就是這樣,any 的應用場景大多是這樣的。但是玩 TS 的朋友要小心哦,不要一碰到不確定的就寫個 any 型別,然後寫了之後還不改,那就把 TS 用成了 AnyScript 了,這就和 JS 一樣了?。所以你看呀,TS 的優秀之處在於,你完全可以在 TS 的環境中寫 JS 還能享受 TS 帶來的各種靜態語言的優勢,所以這麼受歡迎也是可以理解滴。

unknown 型別定義與實戰


unknown 型別和 any 都可以表示任何型別,應用場景也和上面型別,但是它更安全。那麼具體安全在哪裡了?我們通過一個例子來看一看:

let demandOne: any;
let demandTwo: unknown;
複製程式碼


我們拿到了開發需求,但是不清楚具體型別又打算繼續開發時,上面兩種情況都可以使用,但是當我們具體使用這兩個變數的時候,any 型別的變數是可以進行任意進行賦值、例項化、函式執行等操作,但是 unknown 只允許賦值,不允許例項化、函式執行等操作,我們來看個例子:

demandOne = 'Hello, Tuture'; // 可以的
demandTwo = 'Hello, Ant Design'; // 可以的

demandOne.foo.bar() // 可以的
demandTwo.foo.bar() // 報錯
複製程式碼


可以看到,unknown 型別只允許賦值操作,不允許物件取值(Getter)   、函式執行等操作,所以它更安全。

never / 函式型別定義與實戰


never 的字面意思是 “永不”,在 TS 中代表不存在的值型別,一般用於給函式進行型別宣告,函式絕不會有返回值的時候使用,比如函式內丟擲錯誤,我們首先看個例子將講解一下如何給函式進行型別宣告,然後接著我們講  never 型別如何使用:

function responseError(message) {
  // ... 具體操作,接收資訊,丟擲錯誤
}
複製程式碼


對於上面的函式,我們可以使用箭頭函式的形式把它抽象成為形如 (args1, args2, ... , argsn) => returnValue ,我們主要關注點在於函式的輸入和輸出,所以我們在型別宣告的時候把函式的輸入引數的型別和輸出結果的型別定義好就可以了。

我們注意到上面我們定義的函式有一個引數: message  ,並且函式體內根據 message 丟擲對應的錯誤,那麼我們來給它進行型別宣告如下:

function responseError(message: string): never {
  // ... 具體操作,接收資訊,丟擲錯誤
}
複製程式碼


可以看到我們同樣使用了 TS 的冒號語法來進行函式引數和返回值的型別定義,因為 message  一般是一個字串 ID,所以我們給它 string 型別,而這個函式絕不會有返回值,只是單純的丟擲錯誤,所以我們給返回值一個 never 型別。

動手實踐


基本瞭解了型別語言的資料結構之後,我們馬上來寫一點 React 程式碼來實踐我們學到的知識。

我們之前準備的程式碼中可以看到,有兩個假資料陣列 todoListDatauserList ,我們使用之前學到的知識來給這兩個陣列進行型別定義,開啟 src/App.tsx 對其中的內容作出對應的修改如下:

// ...
interface Todo {
  user: string;
  time: string;
  content: string;
  isCompleted: boolean;
}

interface User {
  id: string;
  name: string;
  avatar: string;
}

const todoListData: Todo[] = [
  {
    content: "圖雀社群:匯聚精彩的免費實戰教程",
    user: "mRcfps",
    time: "2020年3月2日 19:34",
    isCompleted: false
  },
  // ...
];

const userList: User[] = [
  // ...
];

// ...
複製程式碼


可以看到,上面我們定義了兩個 interface  Todo 和 User,然後以陣列型別的方式對 todoListDatauserList 進行註解,表示 todoListDataTodo[] 型別,userListUser 型別。

這裡的 interface 我們還沒用提到,我們將馬上在後面講到,可以理解它類似 JS 中的物件,用來組織一組型別,就比如我們這裡  todoList 中單個元素實際上是包含四個屬性的物件,其中前三個屬性為 string 原始型別,最後一個屬性為 boolean 型別,所以我們為了給 單個物件元素進行型別註解,我們使用了 interface

列舉和介面


在上一節中我們提到了 interface ,當時沒有細講,這一節我們就先來細細說一下 interface 是什麼?

Interface


它相當於型別中的 JS 物件,用於對函式、類等進行結構型別檢查,所謂的結構型別檢查,就是兩個型別的結構一樣,那麼它們的型別就是相容的,這在電腦科學的世界裡也被成為 “鴨子型別”。

提示 什麼鴨子型別? 當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。


我們馬上來看一個例子瞭解一個 Interface 是怎麼樣的,比如我們之前物件 Todo ,一個 Todo 物件如下:

const todo = {
  content: '圖雀社群,匯聚精彩的免費技術教程';
  user: 'mRcfps',
  time: '2020年3月2日 19:34',
  isCompleted: false,
}
複製程式碼


現在我們要這個 todo 做一個型別註解,根據之前提到的 “鴨子型別” 的方式,我們可以定義一個 Interface 來為它做註解:

interface Todo {
  content: string;
  user: string;
  time: string;
  isCompleted: boolean;
}

const todo: Todo = {
 // ...
}
複製程式碼


可以看到我們的介面 Todo 內容有四個欄位,並且標註了這四個欄位的型別,比如 contentstring ,這個介面的樣子和 todo 物件是一樣的,所以用 Interface  Todo  來註解 todo 是可行的,用 VSCode 的同學,應該可以看到我們這樣寫之後,編輯器裡面沒有丟擲異常。

可選屬性


上面我們講到 Interface 是用來註解 物件,函式等,那麼我們就有一個場景,一個物件裡面的某些引數我們可能沒有,比如一個待辦事項 Todo,有時候沒有設定 time 時間屬性,那麼修飾這樣一個物件我們該怎麼辦了?幸好 TS 給我們提供了可選屬性這樣一個方便的屬性,使得我們可以方便解決上面的問題,我們來看一下可選屬性該怎麼寫,假如我們上面的那個例子,time 是可選的,那麼我們可以寫出如下這樣:

interface Todo {
  content: string;
  user: string;
  time?: string;
  isCompleted: boolean;
}
複製程式碼


我們看到,只需要在屬性型別修飾冒號左邊加一個問號就可以了,這個時候我們就告訴 TS 編譯器這個 time 屬性是可選的一個型別,所以我們用上面的 Interface Todo 來註解一下沒有 time 屬性的 todo 物件如下:

const todo: Todo = {
  content: '予力內容創作,加速技術傳播',
  user: 'pftom',
  isCompleted: false,
}
複製程式碼


可以看到,使用 VSCode 來跟著教程敲的同學應該發現上面的內容沒有錯誤,型別檢查通過了。

只讀屬性


TS 的 Interface 還有一些額外的屬性比如只讀屬性(readonly),表示用相關帶有隻讀屬性的介面對某個 JS 元素做型別註解的時候,這個 JS 元素相關的屬性被註解為只讀屬性時,我們之後不可以修改這個屬性了,我們來看一個例子:

interface Todo {
  content: string;
  readonly user: string;
  time?: string;
  isCompleted: boolean;
}
複製程式碼


可以看到只讀屬性的新增就是在屬性之前加上 readonly 關鍵字,就可以將 Interface 中的屬性標誌為已讀的,我們來試驗一下這個只讀效果:

const todo: Todo = {
  content: '予力內容創作,加速技術傳播',
  user: 'pftom',
  isCompleted: false,
}

todo.user = 'mRcfps'
複製程式碼


當我們進行上面的修改操作之後,編輯器內會報錯:

型別即正義:TypeScript 從入門到實踐(一)

多餘屬性檢查


我在在 JS 中經常會遇到一個物件,一開始我們知道它有是哪個屬性,但是它的屬性卻可以動態增加,比如我們的 todo 可能還存在 priority 優先順序這樣一個屬性,那麼我們如何定義一個可以註解動態增加屬性物件的 Interface 了?

所幸 TS 提供一個多餘屬性檢查的寫法,使得上面的問題我們也可以解決,我們來看一下一個多餘屬性教程該怎麼定義:

interface Todo {
  isCompleted: boolean;
  [propName: string]: any;
}
複製程式碼


使用類似上面 JS 中的動態屬性賦值的方式我們就可為 Todo 介面加上多餘屬性檢查,這裡我們將其註解為一定擁有  isCompleted 屬性,其他的屬性可以動態新增,因為動態新增的屬性的值型別我們不清楚,所以我們用 any 來表示值型別,它可以是任意型別。我們馬上來試驗一下:

const todo: Todo = {
  content: '予力內容創作,加速技術傳播',
  isCompleted: false,
}

todo.user = 'pftom';
todo.time = '2020-04-04';
複製程式碼


可以看到,上面我們我們的 todo 在定義的時候只有兩個屬性,後面我們額外新增了兩個屬性,發現編輯器裡面也不會報錯,這就是多餘屬性檢查的魅力。

Enum


列舉是 TS 中獨有的概念,在 JS 中沒有,主要用於幫助定義一系列命名常量,常用於給一類變數做型別註解,它們的值是一組值裡面的某一個,比如我們應用中參與建立待辦事項的使用者只有五個人,那麼在建立待辦事項時,此事項的所屬使用者是五人中的某一人。

我們馬上來看一個例子,我們的將這五個使用者放到列舉裡面:

enum UserId {
  tuture,
  mRcfps,
  crxk,
  pftom,
  holy
}
複製程式碼


進而我們可以改進一下我們在上節  Interface 裡面的 Todo 介面,給它的 user 欄位一個更精確的型別註解:

interface Todo {
  content: string;
  user: UserId;
  time: string;
  isCompleted: boolean;
}
複製程式碼


通過上面的例子我們可以看到,todo  裡面的 user 欄位應該是五人之一,它有可能是 tuture ,也有可能是 mRcfps ,我們不知道,所以我們寫了一個列舉 UserId ,並用它來註解 Todouser 欄位。

數字列舉


上面我們的 UserId 中幾個列舉值其實都對應著相應的數字,比如 UserId.tuture 它的值是數字 0UserId.mRcfps 它的值是數字 1 ,以此類推,後面的幾個列舉值分別是數字 234

當然我們也可以手動給其中某個列舉值賦值一個數字,這樣這個列舉值後面的值會依次在這個賦值的數字上遞增,我們來看個例子:

enum UserId {
  tuture,
  mRcfps = 6,
  crxk,
  pftom,
  holy,
}
複製程式碼


上面我們的每個列舉值對應的數字依次是:06789

字串列舉


列舉的值除了是數字還可以是一系列字串,比如:

enum UserId {
  tuture = '66666666',
  mRcfps = '23410977',
  crxk = '25455350',
  pftom = '23410976',
  holy = '58352313',
}
複製程式碼


可以看到,我們給每個列舉值賦值了對於的字串。

異構列舉


當然在一個列舉裡面既可以有字串值也可以有數字:

enum UserId {
  tuture = '66666666',
  mRcfps = 6,
}
複製程式碼

動手實踐


瞭解了 InterfaceEnum 之後,我們馬上運用在我們的專案中來完善我們的待辦事項應用。

隨著內容越寫越多,我們的 src/App.tsx 越來越複雜,所以我們打算把 TodoInput 元件拆到單獨的頁面,在 src 目錄下新建 TodoInput.tsx ,並在裡面編寫如下的內容:

import React, { useState } from "react";
import { Input, Select, DatePicker } from "antd";
import { Moment } from "moment";

import { userList } from "./utils/data";

const { Option } = Select;

enum UserId {
  tuture = "666666666",
  mRcfps = "23410977",
  crxk = "25455350",
  pftom = "23410976",
  holy = "58352313"
}

export interface TodoValue {
  content?: string;
  user?: UserId;
  date?: string;
}

interface TodoInputProps {
  value?: TodoValue;
  onChange?: (value: TodoValue) => void;
}

const TodoInput = ({ value = {}, onChange }: TodoInputProps) => {
  const [content, setContent] = useState("");
  const [user, setUser] = useState(UserId.tuture);
  const [date, setDate] = useState("");

  const triggerChange = (changedValue: TodoValue) => {
    if (onChange) {
      onChange({ content, user, date, ...value, ...changedValue });
    }
  };

  const onContentChange = (e: any) => {
    if (!("content" in value)) {
      setContent(e.target.value);
    }

    triggerChange({ content: e.target.value });
  };

  const onUserChange = (selectValue: UserId) => {
    if (!("user" in value)) {
      setUser(selectValue);
    }

    triggerChange({ user: selectValue });
  };

  const onDateOk = (date: Moment) => {
    if (!("date" in value)) {
      setDate(date.format("YYYY-MM-DD HH:mm"));
    }

    triggerChange({ date: date.format("YYYY-MM-DD HH:mm") });
  };

  return (
    <div className="todoInput">
      <Input
        type="text"
        placeholder="輸入待辦事項內容"
        value={value.content || content}
        onChange={onContentChange}
      />
      <Select
        style={{ width: 80 }}
        size="small"
        defaultValue={UserId.tuture}
        value={user}
        onChange={onUserChange}
      >
        {userList.map(user => (
          <Option value={user.id}>{user.name}</Option>
        ))}
      </Select>
      <DatePicker
        showTime
        size="small"
        onOk={onDateOk}
        style={{ marginLeft: "16px", marginRight: "16px" }}
      />
    </div>
  );
};

export default TodoInput;
複製程式碼


可以看到上面的內容,主要有如下幾個部分的修改:

  • 我們定義了新的 InterfaceTodoInputProps ,它主要用來註解 TodoInput 這個函式式元件的 props 型別,可看到這個介面主要有兩個欄位,一個是 value ,它是 TodoValue 型別,還有一個 onChange ,它是一個函式型別,表示父元件將會傳遞一個 onChange 函式,我們將在之後講解 TS 怎麼註解函式,。
  • 接著我們新增了一個列舉 UserId ,用來概括我們應用的五個使用者的 ID,並且人為的為這五個列舉常量賦了對應的值。
  • 接著我們改進了定義了一個新 TodoValue 介面,它有三個欄位,主要用於標誌 TodoInputProps 中上層元件中可能傳遞下來的值,所以三個欄位都是可選的
  • 最後我們定義了三個響應 InputSelectDatePicker 的函式,onContentChangeonUserChangeonDateOk ,當上層元件沒有傳遞對應的屬性時,使用 setXXX 來更新 React 狀態,否則觸發 triggerChange ,呼叫父元件傳遞下來的 onChange 方法來更新對應的狀態

提示 上面我們從 ./utils/data 匯入了 userList ,以及匯入了 Moment 用來註解 moment 型別的 date ,我們將在接下來的來馬上來建立對於的 ./utils/data 檔案以及安裝對於的 moment


src/TodoInput.tsx 中我們匯入了 Moment 用來註解 onDateOk 的函式引數 date ,接下來我們來安裝它:

npm install moment
複製程式碼
// ...
    "customize-cra": "^0.9.1",
    "less": "^3.11.1",
    "less-loader": "^5.0.0",
    "moment": "^2.24.0",
    "react": "^16.13.0",
    "react-app-rewired": "^2.1.5",
    "react-dom": "^16.13.0",
    // ...
複製程式碼


接著我們來建立對應的 src/utils/data.ts 檔案,把之前在 src/App.tsx 裡面的假資料統一放在這個檔案裡面,然後匯出:

interface Todo {
  user: string;
  time: string;
  content: string;
  isCompleted: boolean;
}

interface User {
  id: string;
  name: string;
  avatar: string;
}

export const todoListData: Todo[] = [
  {
    content: "圖雀社群:匯聚精彩的免費實戰教程",
    user: "mRcfps",
    time: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    content: "圖雀社群:匯聚精彩的免費實戰教程",
    user: "pftom",
    time: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    content: "圖雀社群:匯聚精彩的免費實戰教程",
    user: "Holy",
    time: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    content: "圖雀社群:匯聚精彩的免費實戰教程",
    user: "crxk",
    time: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    content: "圖雀社群:匯聚精彩的免費實戰教程",
    user: "Pony",
    time: "2020年3月2日 19:34",
    isCompleted: false
  }
];

export const userList: User[] = [
  {
    id: "666666666",
    name: "圖雀社群",
    avatar: "https://avatars0.githubusercontent.com/u/39240800?s=60&v=4"
  },
  {
    id: "23410977",
    name: "mRcfps",
    avatar: "https://avatars0.githubusercontent.com/u/23410977?s=96&v=4"
  },
  {
    id: "25455350",
    name: "crxk",
    avatar: "https://avatars1.githubusercontent.com/u/25455350?s=96&v=4"
  },
  {
    id: "23410976",
    name: "pftom",
    avatar: "https://avatars0.githubusercontent.com/u/23410977?s=96&v=4"
  },
  {
    id: "58352313",
    name: "holy",
    avatar: "https://avatars0.githubusercontent.com/u/58352313?s=96&v=4"
  }
];
複製程式碼


拆分了 TodoInput ,並把假資料移動到單獨的檔案之後,我們需要修改 src/App.tsx 對應的部分如下:

import React, { useRef } from "react";

// ...中間一樣

import TodoInput from "./TodoInput";

// ... 中間一樣

import { todoListData } from "./utils/data";

const { Title } = Typography;
const { TabPane } = Tabs;

// 中間一樣

// ... 刪除 TodoInput 部分

// ... TodoList 保持原樣

function App() {
  const callback = () => {};

  const onFinish = (values: any) => {
    console.log("Received values from form: ", values);
  };
  const ref = useRef(null);

  return (
    <div className="App" ref={ref}>
    // ... 中間一樣
          <Form.Item name="todo">
            <TodoInput />
          </Form.Item>
          <Form.Item>
            <Button type="primary" htmlType="submit">
              提交
            </Button>
          </Form.Item>
        </Form>
      </div>
    // ... 中間一樣
		</div>
	);
}

export default App;
複製程式碼


可以看到,上面的內容主要做出瞭如下的修改:

  • 我們刪除了對應的假資料 userListtodoListData 及其 Interface 定義 TodoUser ,轉而從我們建立的 src/utils/data.ts 裡面匯入 todoListData
  • 接著我們刪除了 TodoInput 元件,轉而匯入我們之前建立的  TodoInput 元件
  • 接著我們給 Form 表單部分加上了一個提交按鈕,以及擴充套件了 onFinish 函式
  • 最後我們刪除了一些不再需要的導包

小結


大功告成,這一節中我們學習了介面(Interface)和列舉(Enum),介面主要是對物件等多屬性元素進行型別註解,而列舉是 TS 中獨有的一個概念,在 JS 中沒有,主要用於幫助定義一系列命名常量,常用於給一類變數做型別註解,它們的值是一組值裡面的某一個,最後我們通過改進現有的 Todo 應用來實踐了學到的這兩個概念。

想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。

本文所涉及的原始碼都放在了 Github  或者 Gitee 上,如果您覺得我們寫得還不錯,希望您能給❤️這篇文章點贊GithubGitee 倉庫加星❤️哦~

型別即正義:TypeScript 從入門到實踐(一)

相關文章