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

做者:一隻圖雀
倉庫:GithubGitee
圖雀社區主站(首發):圖雀社區
博客:掘金知乎慕課
公衆號:圖雀社區
聯繫我:關注公衆號後能夠加圖雀醬微信哦
原創不易,❤️點贊+評論+收藏 ❤️三連,鼓勵做者寫出更好的教程。javascript

源起

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

本文所涉及的源代碼都放在了 Github  或者 Gitee 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github** 或 Gitee 倉庫加星❤**️哦~前端

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

代碼準備


咱們接下來要講解的整個 **類型即正義:TypeScript 從入門到實踐 **系列是基於一個實戰項目的,這個實戰項目會貫穿整個系列教程的講解週期,因此咱們要儘量全且精煉的講解 TypeScript 語法知識的同時,還咱們須要一個恰到好處的實戰項目,由於準備項目代碼的過程不是系列教程講解的主線,因此若是你有興趣學習如何搭建 TypeScript React 的開發環境,那麼能夠學習一下咱們的序言教程:react

類型即正義:TypeScript 從入門到實踐(序章)
**
若是你已經對 TypeScript 如何搭建 React 開發環境,配置 Ant Design 組件庫等很熟悉,或者不太感興趣,那麼你也能夠直接克隆咱們爲你準備好的代碼:git

若是你偏心 碼雲,那麼你能夠運行以下命令來獲取初始代碼:github

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

若是你偏心 Github,那麼你能夠運行以下命令來獲取初始代碼:typescript

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


經過上面的命令克隆初始代碼以後,而後把項目跑起來,你應該能夠看到以下效果:

npm

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'
複製代碼


當咱們進行上面的修改操做以後,編輯器內會報錯:


多餘屬性檢查


我在在 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 倉庫加星❤️哦~

相關文章
相關標籤/搜索