您好,登錄后才能下訂單哦!
這篇文章主要介紹“有哪些關(guān)于TypeScript的知識點(diǎn)”,在日常操作中,相信很多人在有哪些關(guān)于TypeScript的知識點(diǎn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”有哪些關(guān)于TypeScript的知識點(diǎn)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
學(xué)習(xí)準(zhǔn)備
開始 TypeScript(以下簡稱 TS)正式學(xué)習(xí)之前,推薦做好以下準(zhǔn)備:
Node 版本 > 8.0
IDE(推薦 VS Code,TS 是微軟推出的,VS Code 也是微軟推出,且輕量。對 TS 代碼更友好)
打開 TypeScript 官網(wǎng)可以看到官方對 TS 的定義是這樣的
JavaScript and More A Result You Can TrustGradual Adoption
這三個(gè)點(diǎn)就很好地詮釋了 TypeScript 的特性。在此之前,先來簡單體驗(yàn)下 TypeScript 給我們的編程帶來的改變。
這是一個(gè) .js 文件代碼:
let a = 123a = '123'
這是 .ts 文件代碼:
let b = 123b = '123'
當(dāng)我們在 TS 文件中試圖重新給 b 賦值的時(shí)候,發(fā)生了錯(cuò)誤,鼠標(biāo)移動到標(biāo)紅處,系統(tǒng)提示:
Type ·"123"' is not assignable to type 'number'
原因是什么呢?
答案很簡單,在 TS 中所有變量都是靜態(tài)類型,let b = 123 其實(shí)就是 'let b:number = 123'。b 只能是 number 類型的值,不能賦值給其他類型。
TypeScript 的優(yōu)勢
TS 靜態(tài)類型,可以讓我們在開發(fā)過程中發(fā)現(xiàn)問題
更友好的編輯器自動提示
代碼語義清晰易懂,協(xié)作更方便
配上代碼來好好感受下這三個(gè)優(yōu)勢帶給我們的編程體驗(yàn)有多直觀,建議邊在編輯器上敲代碼。
先上最熟悉的 JS:
function add(data) { return data.x + data.y }add() //當(dāng)直接這樣寫,在運(yùn)行的時(shí)候才會有錯(cuò)誤告知 add({x:2,y:3})
再上一段 TS 代碼(如果對語法有疑問可以先不糾結(jié),后續(xù)會有講解,此處可以先帶著疑問)
interface Point { x: number, y: number } function tsAdd(data: Point): number { return data.x + data.y }tsAdd() //直接這樣寫,編輯器有錯(cuò)誤提示 tsAdd({ x: 1,y: 123})
當(dāng)我們在 TS 中調(diào)用 data 變量中的屬性的時(shí)候,編輯器會有想 x、y 屬性提示,并且我們直接看函數(shù)外部,不用深入,就能知道 data 的屬性值。這就是 TS 帶給我們相比于 JS 的便捷和高效。
搭建 TypeScript 環(huán)境,可以直接在終端執(zhí)行命令:
npm install -g typescript
然后我們就可以直接 cd 到 ts 文件夾下,在終端運(yùn)行:
tsc demo.ts
tsc 簡而言之就是 typescript complaire,對 demo.ts 進(jìn)行編譯,然后我們就可以看到該目錄下多了一個(gè)同名的 JS 文件,可以直接用 Node 進(jìn)行編譯。
到這里我們就可以運(yùn)行 TS 文件了,但是這只是一個(gè)文件,而且還要先手動編譯成 TS 在手動運(yùn)行 Node,有沒有一步到位的命令呢?當(dāng)然有,終端安裝 ts-node:
npm install -g ts-node
這樣我們可以直接運(yùn)行:
ts-node demo.ts
來運(yùn)行 TS 文件,如果要初始化 ts 文件夾,進(jìn)行 TS 相關(guān)配置,可以運(yùn)行:
tsc --init
關(guān)于相關(guān)配置,這里我們先簡單提下,后面將會分析常用配置,可以先自行打開 tsconfig.json 文件,簡單看下其中的配置,然后帶著疑問繼續(xù)往下看。
正式介紹 TS 的語法之前,還需要再把開篇提到的靜態(tài)類型再來說清楚一些。
const a: number = 123
之前說過,代碼的意思是 a 是一個(gè) number 類型的常量,且類型不能被改變。這里我要說的深層意思是,a 具有 number 的屬性和方法,當(dāng)我們在編輯器調(diào)用 a 的屬性和方法的時(shí)候,編輯器會給我們 number 的屬性和方法供我們選擇。
TS 不僅允許我們給變量定義基礎(chǔ)類型,還可以定義自定義類型:
interface Point { x: number y: number } const a: Point = { x: 2, y: 3 }
把 a 定義為 Point 類型,a 就擁有了 Point 的屬性和方法。而我們把 a 定義為 Point 類型之后,a 必須 Point 上 的 x 和 y 屬性。這樣我們就把 Type 理解的差不多了。
類比于 JavaScript 的類型,TypeScript 也分為基礎(chǔ)類型和引用類型。
原始類型
原始類型分為 boolean、number、string、void、undefined、null、symbol、bigint、any、never
JS 中也有的這里就不多解釋,主要說下之前沒有見過的幾種類型,但是需要注意一下的是我們在聲明 TS 變量類型的時(shí)候都是小寫,不能寫成大寫,大寫是表示的構(gòu)造函數(shù)。
void 表示沒用任何類型,通常我們會將其賦值給一個(gè)沒有返回值的函數(shù):
function voidDemo(): void { console.log('hello world') }
bigint 可以用來操作和存儲大整數(shù),即使這數(shù)已經(jīng)超出了 JavaScript 構(gòu)造函數(shù) Number 能夠表示的安全整數(shù)范圍,實(shí)際場景中使用較少,有興趣的同學(xué)可以自行研究下。
any 指的是任意類型,在實(shí)際開發(fā)中應(yīng)該盡量不要將對象定義為 any 類型:
let a: any = 4 a = '4'
never 表示永不存在的值的類型,最常見的就是函數(shù)中不會執(zhí)行到底的情況:
function error(message: string): never { throw new Error(message) console.log('永不執(zhí)行') }function errorEmitter(): never { while(true){} }
引用類型
對象類型:賦值時(shí),內(nèi)必須有定義的對象屬性和方法
const person: { name: string age: number } = { name: 'aaa' age: 18 }
數(shù)組類型:數(shù)組中每一項(xiàng)都是定義的類型。
const numbers: number[] = [1, 2, 3]
類類型:可以先不關(guān)注寫法,后面還會詳細(xì)講解。
class Peron {} const person: Person = new Person()
類型的介紹差不多就這么些知識點(diǎn),先在腦海里有個(gè)印象,不懂的地方可以繼續(xù)帶著疑問往下看。
之前已經(jīng)講過 TypeScript 的類型和它的類型種類,這一小節(jié)還是想繼續(xù)把有關(guān)類型的知識講全,那么就是類型注解和類型推斷。
let a: number a = 123
上面代碼中這種寫法就是類型注解,通過顯式聲明,來告訴 TS 變量的類型:
let b = 123
這里我們并沒有顯式聲明 b 的類型,但是我們在編輯器中把光標(biāo)放在 b 上,編輯器會告訴我們它的類型。這就是類型推斷。
簡單的情況,TS 是可以自動分析出類型,但是復(fù)雜的情況,TS 無法分析變量類型,我們就需要使用類型注釋。
// 場景一 function add(first,second) { return first + second }const sum = add(1,2) // 場景二function add2(first: nnumber,second: number) { return first + second }const sum2 = add2(1,2)
在場景一中,形參 first、second 的類型 TS 推斷為 any,且函數(shù)的返回值也是推斷為 any,因?yàn)檫@種情況下,TS 無法判斷類型,傳參的時(shí)候可能傳 number 或者 string 等。
場景二中,即使我們沒有定義 sum2 的類型,TS 一樣可以推斷出 number,這是因?yàn)?sum2 是由 first second 求和的結(jié)果,所以它一定是 number。
不管是類型推斷還是類型注解,我們的目的都是希望變量的類型是固定的,這樣不會把 typescript 變成 anyscript。
補(bǔ)充:函數(shù)結(jié)構(gòu)中的類型注解。
// 情況一 function add({ first }: {first: number }): number { return first } // 情況二 function add2({first, second}: {first: number, second: number}): number { return first + second } const sum2 = add({ first: 1, second: 2}) const sum2 = add2({ first: 1, second: 2})
配置文件
之前我們提到過,當(dāng)我們要運(yùn)行 TS 文件時(shí),執(zhí)行命令 tsc 文件名 .ts 就可以編譯 TS 文件生成一個(gè)同名 JS 文件,這個(gè)過程是怎么來的呢,或者如果我們想修改生成的文件名和文件目錄該怎么辦呢?
相信你已經(jīng)心里有答案了,沒錯(cuò),和 webpack 打包或者 babel 編譯一樣,TS 也有一個(gè)編譯配置文件 tsconfig.json。當(dāng)我們執(zhí)行ts --init,文件目錄下就多了一個(gè) TS 配置文件,TS 編譯成 js,就是由 tsconfig 中配置而來。
為了驗(yàn)證下 tsconfig 文件確實(shí)會對 TS 文件編譯做配置,修改里面的:
"removeComments": true //移除文件中的注釋
然后新建一個(gè) demo.ts 文件:
// 這是一個(gè)注釋 const a: number = 123
執(zhí)行 tsc demo.ts,打開 demo.js 文件,發(fā)現(xiàn)注釋并沒有被移除,這是怎么回事,配置文件不生效?
真相是這樣的,當(dāng)我們直接執(zhí)行文件的時(shí)候,并不會使用 tsconfig 中的配置,只有我們直接執(zhí)行 tsc,就會使用 tsconfig 中的配置,直接運(yùn)行 tsc,你就發(fā)現(xiàn)了,amazing!
當(dāng)運(yùn)行 tsc 命令的時(shí)候,直接會先去找到 tsconfig 配置文件,如果沒有做其他改動,會默認(rèn)編譯根目錄下的 TS 文件。
如果想編譯指定文件,則可以在 compilerOptions 配置項(xiàng)同級增加:
"include": ["./demo.ts"]或者"files": ["./demo.ts"]
如果想要不包含某個(gè)文件,則可以同上增加:
"exclude": ["./demo.ts"]
有關(guān)于這一塊的更多配置,可以參考 tsconfig 配置文檔。
下面再來關(guān)注下 compilerOptions 中的屬性,由這個(gè)英文名就知道,這其實(shí)就是指的編譯配置的意思。
"compilerOptions": { "increments": true // 增量編譯,只編譯新增加的內(nèi)容 "target": "es5", // 指定 ECMAScript 目標(biāo)版本: 'ES5' "module": "commonjs", // 指定使用模塊: 'commonjs', 'amd', 'system', 'umd' or 'es2015' "moduleResolution": "node", // 選擇模塊解析策略 "experimentalDecorators": true, // 啟用實(shí)驗(yàn)性的ES裝飾器 "allowSyntheticDefaultImports": true, // 允許從沒有設(shè)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入。 "sourceMap": true, // 把 ts 文件編譯成 js 文件的時(shí)候,同時(shí)生成對應(yīng)的 map 文件 "strict": true, // 啟用所有嚴(yán)格類型檢查選項(xiàng) "noImplicitAny": true, // 在表達(dá)式和聲明上有隱含的 any類型時(shí)報(bào)錯(cuò) "alwaysStrict": true, // 以嚴(yán)格模式檢查模塊,并在每個(gè)文件里加入 'use strict' "declaration": true, // 生成相應(yīng)的.d.ts文件 "removeComments": true, // 刪除編譯后的所有的注釋 "noImplicitReturns": true, // 不是函數(shù)的所有返回路徑都有返回值時(shí)報(bào)錯(cuò) "importHelpers": true, // 從 tslib 導(dǎo)入輔助工具函數(shù) "lib": ["es6", "dom"], // 指定要包含在編譯中的庫文件 "typeRoots": ["node_modules/@types"], "outDir": "./dist", // 生成文件目錄 "rootDir": "./src" // 入口文件 },
接口是用來自定義類型或者為我們的第三方 JS 庫做翻譯的一種方式。之前的代碼中已經(jīng)使用到了接口,其實(shí)就是用來描述類型的。每個(gè)人都有姓名和年齡,那我們就會這樣去約束 person。
interface Person { name: string age: number }let person: Person
當(dāng)我們進(jìn)行這樣的類型約束的時(shí)候,person 這個(gè)對象在初始化的時(shí)候就必須要有 name 和 age,初始化有兩種方式,再來看下其中的不同支出:
// 承接上面的代碼 // 第一種初始化方式 person = { name: 'aaa', age: 18 } // 第二種初始化方式 let p = { name: 'aaa', age: 18, sex: 'male' } person = p
第一種方式和第二種方式相比,p 對象中多了一個(gè) sex 屬性,然后賦值給了 person,編輯器沒有提示錯(cuò)誤,但是如果在第一種方式中添加一個(gè) sex 屬性則會報(bào)錯(cuò),這是為什么呢?
這是因?yàn)?,?dāng)我們直接賦值(也就是通過第一種方式)的時(shí)候,TS 會進(jìn)行強(qiáng)類型檢查,因此必須和接口定義的類型一致才行。
注意我們上面提到一致,一致的意思是,屬性名和屬性值類型一致,且屬性個(gè)數(shù)不多不少。而當(dāng)使用第二種方式進(jìn)行賦值的時(shí)候,則會進(jìn)行弱檢查。屬性個(gè)數(shù)一致會較弱,表現(xiàn)在,當(dāng)屬性多了一個(gè)的時(shí)候,不會有語法錯(cuò)誤。
此時(shí)我們會產(chǎn)生一個(gè)疑問,如果我們想讓第一種方式也能做到和第二種方式一樣,或者說,每個(gè)人年齡和姓名是必須的,但是所在城市 city 是選填的,那該如何呢?我們可以用可選屬性描述。
interface Person { name: string age: number city?: string }
如果這樣的話,我們在調(diào)用 p 屬性的時(shí)候就可以看到 city 屬性可能是 string,也可能是 undefined:
不僅如此,我們還希望,age 屬性是不可修改的,readonly 屬性自然就派上用場了,當(dāng)你試圖修改定義了 readonly 屬性的時(shí)候,那么編輯器就會發(fā)出警告:
interface Person { name: string readonly age: number city?: string }let person: Person = { name: 'aaa', age: 18 }// person.age = 18
當(dāng)然這還沒結(jié)束,如果有一天,還想再擴(kuò)展一個(gè)接口,是公司職員的接口,但是職員接口類肯定有 Person 類的所有信息,再擴(kuò)展一個(gè) id,又該如何呢?這時(shí)候繼承(extends)就上場了。
interface Employee extends Person { id: number }
接口還可以用來約束類,讓定義的類必須有某種屬性或者方法,這時(shí)候關(guān)鍵字就不是 extends,而是 implements。
interface User { name: string getName(): string}class Student implements User { name = 'aaa' getName() { return this.name }}
interface VS type
interface 和 type 作用看起來似乎是差不多的,都是用來定義類型,接下讓我們看下它的相同點(diǎn)與不同點(diǎn)。
相同點(diǎn):
1. 都可以描述對象或函數(shù)
interface Person { name: string age: number}type Person1 = { //type 定義類型有等號 name: string age: number}interface getResult { (value: string): void }type getResult1 = (value: string): void
2. 都可以實(shí)現(xiàn)繼承
// interface 繼承 interface interface People extends Person { sex: string }// interface 繼承 typeinterface People extends Person1 { sex: string }// type 繼承 type type People1 = Person1 & { sex: string }// type 繼承 interface type People1 = Person & { sex: string }
不同點(diǎn):
1. type 可以聲明基本類型、聯(lián)合類型,interface 不行
// 基本類型 type Person = string // 聯(lián)合類型type User = Person | number
2. interface 可以類型合并
interface People { name: string age: number }interface People { sex: string }//People 最終類型為 { name: string age: number sex: string }
3. interface 可以定義可選和只讀屬性(之前講過,這里不再贅述)
接口的基礎(chǔ)知識差不多就介紹完了,當(dāng)然接口在實(shí)際開發(fā)場景中應(yīng)用會更復(fù)雜,如果你還有很多疑惑,接著往下看,下面的講解將會解答你的疑惑。
聯(lián)合類型和類型保護(hù)
和其他分享資料不同,我希望每一個(gè)知識點(diǎn)都能先讓你先有所疑惑,啟發(fā)你的思考,然后我再慢慢解決你的疑惑,這樣我相信你會記憶更加深刻,否則可能將成效見微。
閑話少敘,直接上一段代碼:
interface Bird { fly: boolean sing: () => {} }interface Dog { fly: boolean bark: () => {} }function trainAnimal(animal: Bird | Dog) { // animal.sing() }
上面代碼中我定義了兩個(gè)類型,一個(gè) Bird 類型,一個(gè)是 Dog 類型。函數(shù) trainAnimal 的形參接收一個(gè) animal 的參數(shù),這個(gè)參數(shù)可能是 Bird 類型,也可能是 Dog 類型,這就是聯(lián)合類型。當(dāng)在函數(shù)中調(diào)用的時(shí)候,編輯器給的提示只有 fly:
這還真有點(diǎn)東西,但是仔細(xì)想想,就覺得只有 fly 沒毛病。因?yàn)槁?lián)合類型的 animal 無法確定具體是哪個(gè)類型,因此只能提示共有的屬性。而獨(dú)有方法經(jīng)過聯(lián)合類型阻隔之后是無法進(jìn)行語法提示。如果我們強(qiáng)行調(diào)用某個(gè)類型獨(dú)有的方法,可以看到編輯器會有錯(cuò)誤提示。
如果確實(shí)需要使用獨(dú)有方法,該當(dāng)如何?
這就需要類型保護(hù)了,確實(shí),如果聯(lián)合類型只能調(diào)用共有方法,似乎看起來也用處不是很大,好在有類型保護(hù)。類型保護(hù)也有好多種,我們分別來介紹下。
1. 類型斷言
function trainAnimal(animal: Bird | Dog) { if (animal.fly) { (animal as Bird).sing() } else { (animal as Dog).bark() }}
上面代碼中通過一個(gè) as 關(guān)鍵字實(shí)現(xiàn)了類型斷言。因?yàn)榘凑者壿?,我們知道,如果?fly 方法,那么 animal 一定是 Bird 類型,但是編輯器不知道,所以通過 as 告訴編輯器此時(shí) animal 就是 Bird 類型,Dog 類型的確定也是同理。
2. 通過 in 來類型斷言,TS 語法檢查就能確定參數(shù)類型
function trainAnimalSecond(anmal: Bird | Dog ) { if ('sing' in animal) { animal.sing() }}
3. 通過 typeof 來做類型保護(hù)
function add(first: string | number, second: string | number) { if (typeof first === 'string' || typeof second === 'string') { return `first:${first}second:${second}` } return first + second }
上面代碼中如何沒有 if 里面的邏輯,直接進(jìn)行判斷,編輯器則會給錯(cuò),因?yàn)槿绻菙?shù)字和字符串相加,則可能存在錯(cuò)誤,因此通過 typeof 來確定,當(dāng) first 和 second 都是數(shù)字的時(shí)候,進(jìn)行相加。
4. 通過 instanceof 來類型保護(hù)
class NumberObj { count: number}function addSecond(first: object | NumberObj, second: object | NumberObj) { if (first instanceof NumberObj && second instanceof NumberObj) { return first.count + second.count }}
在 TS 中,類不僅可以用來實(shí)例化對象,也可以用來定義變量類型,當(dāng)一個(gè)對象被一個(gè)類定義以后,表明這個(gè)對象的值就是這個(gè)類的實(shí)例,關(guān)于類這一塊的寫法有疑問,可以查閱下 ES7 相關(guān)內(nèi)容,這里不做過多講解。
從代碼中我們可以看出,通過 instanceof 來確定具有聯(lián)合類型的形參是否是類的類型,當(dāng)然這里如果要用 instanceof 來判斷,我們的自定義類型定義只能用 class。如果是 interface 定義的類型,使用 instanceof 則會報(bào)錯(cuò)。
枚舉類型
枚舉這個(gè)概念,我們在 JS 中就已經(jīng)接觸的比較多了,關(guān)于概念也不就不做過多的講解,直接上一段代碼。
const Status = { OFFLINE: 0, ONLINE: 1, DELETED: 2 }function getStatus(status) { if (status == Status.OFFLINE) { return 'offline' } else if (status == Status.ONLINE) { return 'online' } else if (status == Status.DELETED) { return 'deleted' } return error }
這是我們在 JS 中比較常見的寫法,TS 中也有枚舉類型,而且比 JS 的更好用。
enum Status { OFFLINE, ONLINE, DELETED}// 方式一 const status = Status.OFFLINE // 0 // 方式二 const status = Status[0] // OFFLINE
通過上面的代碼可以看出,TS 的枚舉類型默認(rèn)會有賦值,而且寫法也很簡單。再看方式一和方式二對枚舉類型的使用,我們可以看出,TS 枚舉類型還支持正反調(diào)用。
剛才說到枚舉類型默認(rèn)有值,如果我想改默認(rèn)值又該如何呢?請看下面的代碼:
enum Status { OFFLINE = 3, ONLINE, DELETED}const status = Status.OFFLINE // 3 const status = Status.ONLINE // 4 enum Status1 { OFFLINE = 6, ONLINE = 10, DELETED}const status = Status.OFFLINE // 6 const status = Status.ONLINE // 10 const status = Status.DELETED // 11
由上可以看出,TS 枚舉類型支持自定義值,且后面的枚舉屬性沒有賦值的話,會在原來的基礎(chǔ)上遞增。
上面我們說到 enum 支持雙向使用,為什么它如此之秀,怎么靈活呢,我們看下枚舉類型編譯成 JS 后的代碼:
var Status; (function (Status) { Status[Status["OFFLINE"] = 6] = "OFFLINE"; Status[Status["ONLINE"] = 10] = "ONLINE"; Status[Status["DELETED"] = 12] = "DELETED"; })(Status || (Status = {}))
函數(shù)泛型
泛型在 TS 的開發(fā)中使用非常廣泛,因此這一節(jié),同樣會由淺入深,先看代碼:
function result(first: string | number, second: string | number) { return `${first} + ${second}` }join('1', 1) join(1,'1')
這是我們之前講過的聯(lián)合類型,兩個(gè)參數(shù)既可以是數(shù)字也可以字符串。
但是現(xiàn)在我有個(gè)需求是這樣的,如果 first 是字符串,則 second 只能是字符串,同理 first 是數(shù)字,則 second。如果不知道泛型,我們只能在函數(shù)內(nèi)部去進(jìn)行邏輯約定,但是泛型一出手,問題就迎刃而解。
function result<T>(first: T,second: T) { return `${first} + ${second}` }join<number>(1,1) join<string>('1','1')
通過在函數(shù)中定義一個(gè)泛型 T(名字可以自定義,一般用 T),這樣的話,我們就可以約束 first,second 類型一致,當(dāng)我們試圖調(diào)用的時(shí)候?qū)崊㈩愋筒灰恢碌臅r(shí)候,那么編輯器就會報(bào)錯(cuò)。
function map<T>(params: T[]) { return params }map([1])
這種形式也是可以的,雖然調(diào)用的時(shí)候沒有顯示定義 T,但是 TS 可以推斷出 T 的類型。T[] 是數(shù)組一種定義類型的方式,表明數(shù)組每個(gè)值的類型。
注意:Array 這種形式在 3.4 之后,會有警告。統(tǒng)一使用方括號形式。
這是單一泛型,但實(shí)際場景中往往是多個(gè)泛型:
function result<T, U>(first: T,second: U) { return `${first} + ${second}` }join<number,string>(1,'1') join(1, '1') //這種形式也可
泛型如此之好用,肯定不可能只在函數(shù)中使用,因此接下來再來說下類中使用泛型:
class DataManager { constructor(private data: string[] | number[]) {} getItem(index: number): string | number { return this.data[index] }}const data = new DataManager([1]) data.getItem(0)
DataManager 類中構(gòu)造函數(shù)通過聯(lián)合類型來定義 data 的類型,這在復(fù)雜的業(yè)務(wù)場景中顯然是不可取的,因?yàn)槿绻覀円膊淮_定類型,在傳參之前,那么只能寫更多的類型或者定義成 any 類型,這就顯得很不靈活,這時(shí)候我們想到了泛型,是否可以應(yīng)用到類中呢?
答案是肯定的。
class DataManager<T> { constructor(private data: <T>) {} getItem(index: number): <T> { return this.data[index] }}const data = new DataManager([1]) // const data = new DataManager<number>([1]) //直觀的寫法,和上面等價(jià) data.getItem(0)
看起來好像已經(jīng)很靈活了,但是還有一個(gè)問題,沒有規(guī)矩不成方圓,函數(shù)編寫者允許調(diào)用者具有傳參靈活度,但是需要符合函數(shù)內(nèi)部的一些邏輯,也就是說之前函數(shù) return this.data[index],但是現(xiàn)在函數(shù)邏輯里面,返回的是 this.data[index].name,也就是函數(shù)調(diào)用者可以傳 T 類型進(jìn)來,但是每一項(xiàng)必須要有 name 屬性,這又該當(dāng)如何?
那么我們可以再定義一個(gè)接口,讓 T 繼承接口,這樣既能保持靈活度,又能符合函數(shù)邏輯。
interface Item { name: string }class DataManager<T extends Item> { constructor(private data: T[]) {} getItem(index: number): number { return this.data[index].name }}const data = new DataManager([ name: 'dell' ])
講到這里,泛型差不多結(jié)束了,但是還有一個(gè)疑問,上面number | string 這種聯(lián)合類型想用泛型來約束,該怎么寫呢,也就是 T 只能是 string 或者 number。
class DataManager<T extends number | string> { constructor(private data: T[]) {} getItem(index: number): T { return this.data[index] }}
命名空間
講到這里,我們之前已經(jīng)新建了很多的 demo 文件,不知道你有沒有發(fā)現(xiàn)這樣一個(gè)奇怪的現(xiàn)象。
demo.ts
let a = 123// dosomething
demo1.ts
let a = '123'
當(dāng)我們在 demo1.ts 文件中再去定義 a 這個(gè)變量的時(shí)候,a 會標(biāo)紅,告訴我們 a 已經(jīng)被聲明了 number 類型,這是為什么呢?
我們明明在 demo1.ts 文件中沒有定義過 a,再仔細(xì)看下提示,它告訴我們已經(jīng)在 demo.ts 中定義過了。對 JS 很熟練的伙伴一定知道了,應(yīng)該是模塊化的問題。
沒錯(cuò),TS 跟 JS 一樣,一個(gè)文件中不帶有頂級的 import 或者 export 聲明,它的內(nèi)容是全局可見的,換句話說,如果我們文件中帶有 import 或者 export,則是一個(gè)模塊化。
export const let a = '123'
這樣就沒有問題了,我們再看下下面這段代碼:
class A { // do something } class B { // do something } class C { // do something } class D { constructor() { new A() new B() new C() } }
代碼中,我定義了四個(gè)類,上面提到,如果我把 D 這個(gè)類通過 export 導(dǎo)出,這樣其他文件中就可以繼續(xù)使用 A 或者其他幾個(gè)類名了,但是我現(xiàn)在有個(gè)需求是這樣的,我不想把 A、B、C 三個(gè)類暴露出去,而且在外面能不能通過想通過對象的方式去調(diào)用 D 這個(gè)類。namespace 登場,看下代碼:
namespace Total{ class A { // do something } class B { // do something } class C { // do something } export class D { constructor() { new A() new B() new C() } } } Total.D
這樣寫就可以了,通過 namespace 就只能調(diào)用到 D。如果還想調(diào)用其他類,只需要在前面去 export 這個(gè)類就好了。
namespace 在實(shí)際開發(fā)中,我們一般用在寫一些 .d.ts 文件。也就是 JS 解釋文件。
命名空間本質(zhì)上是一個(gè)對象,它的作用就是將一系列相關(guān)的全局變量變成一個(gè)對象的屬性,再看下上面的代碼編譯成 JS 是怎么樣的。
var Total; (function (Total) { var A = /** @class */ (function () { function A() { } return A; }()); var B = /** @class */ (function () { function B() { } return B; }()); var C = /** @class */ (function () { function C() { } return C; }()); var D = /** @class */ (function () { function D() { new A(); new B(); new C(); } return D; }()); Total.D = D;})(Total || (Total = {}));Total.D;
從上面可以看出,通過一個(gè)立即執(zhí)行函數(shù)并且傳了一個(gè)變量進(jìn)去,然后把導(dǎo)出的方法掛載在變量上,這樣就可以在外面通過對象屬性的方式調(diào)用類。
最后再補(bǔ)充下 declare,它的作用是,為第三方 JS 庫編寫聲明文件,這樣才可以獲得對應(yīng)的代碼補(bǔ)全和接口提示:
//常用的聲明類型 declare function 聲明全局方法 declare class 聲明全局類 declare enum 聲明全局枚舉類型 declare global 擴(kuò)展全局變量 declare module 擴(kuò)展模塊
也可以使用 declare 做模塊補(bǔ)充。下面摘自官方的一個(gè)示例:
// observable.ts export class Observable<T> { // ... implementation left as an exercise for the reader ... }// map.tsimport { Observable } from "./observable"; declare module "./observable" { interface Observable<T> { map<U>(f: (x: T) => U): Observable<U>; }}Observable.prototype.map = function (f) { // ... another exercise for the reader }// consumer.tsimport { Observable } from "./observable"; import "./map"; let o: Observable<number>; o.map(x => x.toFixed());
代碼的意思在 map.js 中定制一個(gè)文件,補(bǔ)充你想要的類型 map 方法并實(shí)現(xiàn)函數(shù)掛載在 Observable 原型上,然后在 consumer.ts 就可以使用 Observable 類型里面的 map。
類的裝飾器
裝飾器我們在 JS 就已經(jīng)接觸比較久了,并且在我的另一篇 Chat《寫給前端同學(xué)容易理解并掌握的設(shè)計(jì)模式》中也詳細(xì)講解了裝飾器模式,對設(shè)計(jì)模式感興趣的同學(xué),歡迎訂閱。裝飾器本質(zhì)上就是一個(gè)函數(shù)。@description 這種語法其實(shí)就是一個(gè)語法糖。TS 和 JS 裝飾器使用大同小異,先看一個(gè)簡單的例子:
function Decorator(constructor: any) { console.log('decorator') }@Decorator class Demo{} const text = new Test()
當(dāng)我們覺得完美的時(shí)候,編輯器給了我們一個(gè)標(biāo)紅:
其實(shí)裝飾器是一個(gè)實(shí)驗(yàn)性質(zhì)的語法,所以不能直接使用,需要打開實(shí)驗(yàn)支持,修改 tsconfig 的以下兩個(gè)選項(xiàng):
"experimentalDecorators": true, "emitDecoratorMetadata": true,
修改完配置之后,就發(fā)現(xiàn)終端正確輸出了。
但是這里我還要再拋出一個(gè)問題,裝飾器的運(yùn)行時(shí)機(jī)是什么時(shí)候呢,是在類實(shí)例化的時(shí)候嗎?
其實(shí)裝飾器在類創(chuàng)建的時(shí)候就已經(jīng)運(yùn)行裝飾器了,可以自行注釋掉實(shí)例化語句,再運(yùn)行,看控制臺是否有 log。
類的裝飾器修飾函數(shù)接受的參數(shù)是類的構(gòu)造函數(shù),我們可以改一下 Decorator 來驗(yàn)證一下:
function Decorator(constructor: any) { constructor.prototype.getResult = () => { console.log('constructor') }}@Decorator class Demo{} const text = new Test() text.getResult()
控制臺正確打印出 constructor 就可以證明接收的參數(shù)確實(shí)是類的構(gòu)造函數(shù)。上面的代碼中我們只在類中使用了一個(gè)裝飾器,但其實(shí)可以給一個(gè)類使用多個(gè)裝飾器,寫法如下:
@Decorator @Decorator1 class Demo{}
多個(gè)裝飾器執(zhí)行順序?yàn)橄认潞笊稀?/p>
上面的裝飾器寫法,我們把整個(gè)函數(shù)都給了類做裝飾,但是實(shí)際情況是,我函數(shù)有一些邏輯,是不給類裝飾使用的,那么我們寫成一個(gè)工廠模式去給類裝飾:
function Decorator() { // do something return function (constructor: any) { console.log('descorator') }}@Decorator()class Test()
通過這樣,我們可以傳一些參數(shù)進(jìn)去,然后函數(shù)內(nèi)部去控制裝飾器的裝飾。
不知道你有沒有發(fā)現(xiàn),我們在驗(yàn)證裝飾器參數(shù)的時(shí)候,當(dāng)我們通過類的實(shí)例去調(diào)用我們掛載在裝飾器原型的方法的時(shí)候,雖然沒有報(bào)錯(cuò),但是編輯器沒有給我們提示,這是很不符合我們預(yù)期的。上面那種裝飾器寫法很簡單,但很直觀。
但在 TS 中我們往往是像下面這種方式使用的,而且也能解決上面提到的那個(gè)問題:
function Decorator() { return function <T extends new (...args: any[]) => any>(constructor: T) { return class extends constructor{ name = 'bbb' getName } }} const Test = Decorator()( class { name: string constructor(name: string) { console.log(this.name,'1') this.name = name console.log(this.name,'2') }})const test = new Test('aaa') console.log(test.getName())
我們把之前的代碼大變樣,看起來似乎高大上了許多,但是理解起來也挺有難度的。別急,讓我來一一進(jìn)行解釋。
<T extends new (...args: any[]) => any>
這個(gè)是一個(gè)泛型,T 繼承了一個(gè)構(gòu)造函數(shù)也可以說是繼承了一個(gè)類,構(gòu)造函數(shù)參數(shù)是一個(gè)展開運(yùn)算符,表示接收多個(gè)參數(shù)。
這樣泛型 T 就可以用來定義 constructor。而 Decorator 函數(shù),跟上面一樣,我們寫成函數(shù)柯里化形式,并且把類作為參數(shù)傳遞進(jìn)去,摒棄了之前的語法糖,這樣我們在調(diào)用裝飾在類上的方法的時(shí)候編輯器就能給我們提示。
上一節(jié),分享完了類的裝飾器,大家肯定對裝飾器意猶未盡,這一小節(jié),再分享下給類的方法裝飾,先上個(gè)代碼,來看下:
function getNameDecorator( target: any, key: string, descriptor: PropertyDescriptor ) { console.log(target); } class Test { name: string constructor(name: string) { this.name = name } @getNameDecorator getName() { return this.name } }const test - new Test('aaa') console.log(test.getName())
這就實(shí)現(xiàn)了給類的方法進(jìn)行裝飾,當(dāng)我們給類的普通方法進(jìn)行裝飾的時(shí)候,裝飾器函數(shù)中接收的參數(shù) target 對應(yīng)的是類的 prototype,key 是裝飾的普通方法的名字。
注意,我上面說的是普通方法。和類的裝飾器一樣,方法裝飾器的執(zhí)行時(shí)機(jī)同樣是當(dāng)方法被定義的時(shí)候。
剛才我已經(jīng)強(qiáng)調(diào)了普通方法,接下來我就要說靜態(tài)方法了。
class Test { name: string constructor(name: string) { this.name = name } @getNameDecorator static getName() { return this.name }}
靜態(tài)方法的裝飾器函數(shù)中,第一個(gè)參數(shù) target 對應(yīng)的是類的構(gòu)造函數(shù)。
類的方法裝飾器函數(shù)中,我們還有一個(gè)參數(shù)沒有講,那就是 descriptor。
不知道你有沒有發(fā)現(xiàn),這個(gè)函數(shù)接收三個(gè)參數(shù),而且第三個(gè)參數(shù)還是 descriptor,有點(diǎn)像 Object.defineProperty 這個(gè) API,當(dāng)我們在函數(shù)中調(diào)用 descriptor 的時(shí)候,編輯器會給我們提示。
這幾個(gè)屬性和 Object.defineProperty 中的 descriptor 可設(shè)置屬性一樣,沒錯(cuò),功能也是一樣的.比如,我們不想在外部,getName 方法被重寫,那么我們可以這樣:
function getNameDecorator( target: any, key: string, descriptor: PropertyDescriptor ) { console.log(target); descriptor.writable = false }
當(dāng)你試圖這樣去修改它的時(shí)候,運(yùn)行編譯后文件將會報(bào)錯(cuò):
const test = new Test('aaa') console.log(test.getName()) test.getName = () => { return 'aaa' }
這是運(yùn)行結(jié)果:
訪問器裝飾器
在 ES6 的 class 中新增訪問器,通過 get 和 set 方法訪問屬性,如果上面的知識點(diǎn)你都消化了,那么訪問器裝飾器的用法也是如出一轍。
function visitDecorator( target: any, key: string, descriptor: PropertyDescriptor ){} class Test { provate _name: string constructor(name: string) { this._name = name } get name() { return this._name } @visitDecorator set name() { this._name = name }}
訪問器裝飾器的用法跟類的普通方法裝飾器用法差不多,這里就不展開來講了。同樣地,在類中,我們也可以給屬性添加裝飾器,參數(shù)添加裝飾器。
裝飾器業(yè)務(wù)場景使用
之前我們花了比較長的篇幅來介紹裝飾器,這一小節(jié),將跟大家分享下實(shí)際業(yè)務(wù)場景中,裝飾器的使用。首先來看這樣一段代碼:
const uerInfo: any = undefined class Test { getName() { return userInfo.name } getAge() { return userInfo.name }}const test = new Test() test.getName()
這段代碼不用運(yùn)行,我們都能知道,會報(bào)錯(cuò),因?yàn)?userInfo 沒有 name 屬性。因此如果我們想要不報(bào)錯(cuò),就會寫成這樣:
class Test { getName() { try { return userInfo.name } catch (e) { console.log('userInfo.name 不存在') } } getAge() { try { return userInfo.age } catch (e) { console.log('userInfo.age 不存在') } }}
把類改成這樣,似乎就沒有問題了,為什么說似乎呢?
那是因?yàn)檫\(yùn)行雖然沒有問題,但是如果我們還有很多類似于這樣的方法,我們是否要重復(fù)處理錯(cuò)誤呢?能否用到之前講的裝飾器來處理錯(cuò)誤:
const userInfo: any = undefined function catchError( target: any, key: string, descriptor: PropertyDescriptor){ const fn = descriptor.value descriptor.value = function() { try { fn() } catch (e) { console.log('userinfo 出問題啦') } }}class Test { @catchError getName() { return userInfo.name } @catchError getAge() { return userInfo.age }}
這樣我們就把捕獲異常的邏輯提取出來了,通過裝飾器來復(fù)用。
但是和我們之前寫的還有點(diǎn)差異,就是報(bào)錯(cuò)信息都一樣,我們不知道具體是哪個(gè)函數(shù)報(bào)的錯(cuò),也就是說,我們希望裝飾器函數(shù)可以接收一個(gè)參數(shù),來完善報(bào)錯(cuò)信息,這樣的話,我們就可以用到講過的,把裝飾器包裝成一個(gè)工廠函數(shù),代碼如下:
function catchError(msg: string) { return function ( target: any, key: string, descriptor: PropertyDescriptor ){ const fn = descriptor.value descriptor.value = function() { try { fn() } catch (e) { console.log(`userinfo.${msg} 出問題啦`) } } }}class Test { @catchError('name') getName() { return userInfo.name } @catchError('age)' getAge() { return userInfo.age }}
這樣我們的代碼就能滿足我們的需求了,后面我們再添加其他函數(shù)函數(shù),也可以用裝飾器對其進(jìn)行裝飾。
項(xiàng)目中應(yīng)用 TypeScript
腳手架搭建一個(gè) TypeScript
現(xiàn)在的開發(fā)越來越專業(yè),一般我們初始化一個(gè)項(xiàng)目,如果不用腳手架進(jìn)行開發(fā)的話,需要自己去配置一大堆東西,比如 package.json、.gitignore,還有一些構(gòu)建工具,像 webpack 等以及他們的配置。
而當(dāng)我們?nèi)ナ褂?TypeScript 編寫一個(gè)項(xiàng)目的時(shí)候,還需要配置 TypeScript 的編譯配置文件 tsconfig 以及 tslint.json 文件。
如果我們只是想做一個(gè)小項(xiàng)目或者只想學(xué)習(xí)這塊的開發(fā),那前期的磨刀準(zhǔn)備工作將讓很多人望而卻步,一頭霧水。因此,一個(gè)腳手架工具就可以幫我們把刀磨好,而且磨的錚鮮亮麗的,這個(gè)工具就是 TypeScript Library Starter。讓我們一起來了解下。
查看它的官網(wǎng),我們知道這是一個(gè)以 TypeScript 為基礎(chǔ)的開源腳手架工具,幫助我們快速開始一個(gè) TypeScript 項(xiàng)目,使用方法如下:
git clone https://github.com/alexjoverm/typescript-library-starter.git ts-project cd ts-projectnpm install
這幾行命令的意思是,把代碼拉下來然后給項(xiàng)目重命名。進(jìn)入到項(xiàng)目,通過 npm install 去給項(xiàng)目安裝依賴,然后我們來看下我們的文件目錄:
├── package.json // 項(xiàng)目配置文件
├── rollup.config.ts // rollup 配置文件 ├── src // 源碼目錄 ├── test // 測試目錄 ├── tools // 發(fā)布到 GitHup pages 以及 發(fā)布到 npm 的一些配置腳本工具 ├── tsconfig.json // TypeScript 編譯配置文件 └── tslint.json // TypeScript lint 文件
TypeScript library starter 創(chuàng)建的項(xiàng)目確實(shí)集成了很多優(yōu)秀的開源工具,包括打包、單元測試、格式化代碼等,有興趣的同學(xué)可以自行深入研究下。
還有需要介紹的是,TypeScript library starter 在 package.json 中幫我們配置了一套完整的前端工作流:
npm run lint:使用 TSLint 工具檢查 src 和 test 目錄下 TypeScript 代碼的可讀性、可維護(hù)性和功能性錯(cuò)誤。
npm start:觀察者模式運(yùn)行 rollup 工具打包代碼。
npm test:運(yùn)行 Jest 工具跑單元測試。
npm run commit:運(yùn)行 commitizen 工具提交格式化的 git commit 注釋。
npm run build:運(yùn)行 rollup 編譯打包 TypeScript 代碼,并運(yùn)行 typedoc 工具生成文檔。
其他一些命令在我們?nèi)粘i_發(fā)中使用不是非常多,有需要的同學(xué)可以再自行去了解。
現(xiàn)在我們的前端項(xiàng)目基本都是使用框架進(jìn)行開發(fā),今天我就介紹如何使用 React + TypeScript 進(jìn)行 React 項(xiàng)目開發(fā)。當(dāng)然這里我們還是會使用 React 提供的腳手架迅速搭建項(xiàng)目框架,為了避免你本地之前的腳手架版本影響 TypeScript 的開發(fā),建議先執(zhí)行:
npm uninstall create-react-app
然后執(zhí)行官方提供的 React TypeScript 生成命令:
npx create-react-app react-project --template typescript --use-npm
這個(gè)命令的意思是下載最新腳手架(如果當(dāng)前環(huán)境沒有這個(gè)腳手架的話),然后通過 create-react-app 腳手架去生成以 typescript 為開發(fā)模板的項(xiàng)目,項(xiàng)目名字叫 react-project,并通過 npm 去安裝依賴,如果沒有 --use-npm 則會默認(rèn)是使用 Yarn。
項(xiàng)目搭建完成之后,我們把文件整理下,刪除一些我們不用的文件,同時(shí)把相關(guān)引用也刪除,最終文件目錄如下:
當(dāng)我們使用 TS 去寫 React 的時(shí)候, jsx 就變成了 tsx。在 APP.tsx 文件中:
const App: React.FC = () => { return <div className="App"></div> }
通過 React.FC 給函數(shù)定義了一個(gè) React.FC 的函數(shù)類型,這是 React 中定義的函數(shù)類型。
前端 UI 開發(fā),現(xiàn)在市面上也有很多封裝好的框架,讓我們可以快速搭建一個(gè)頁面,這里我們選用 ant-design,這個(gè)框架也是使用 TypeScript 進(jìn)行開發(fā)的,所以我們使用它進(jìn)行開發(fā)的時(shí)候,會有很多類型可以供我們使用,因此使用它去鞏固我們剛學(xué)習(xí)的 TypeScript 知識點(diǎn)會有更多的好處。
首先讓我們來安裝下這個(gè)組件庫:
npm install antd --save
安裝好之后,再 index.tsx 中引入 CSS 樣式:
import 'antd/dist/antd.css'
接下來我們?nèi)憘€(gè)登錄頁面,首頁新建一個(gè) login.css:
.login-page { width: 300px; padding: 20px; margin: 100px auto; border: 1px solid #ccc; }
然后我們?nèi)?antd-design 官網(wǎng),把登錄組件代碼復(fù)制到我們的 App.ts 中:
import React from "react"; // import ReactDOM from 'react-dom' import "./login.css"; // function App() { // return <div className="login-page">Hello world</div>; // } // export default App; import { Form, Input, Button, Checkbox } from "antd"; // import { Store } from "antd/lib/form/interface"; import { ValidateErrorEntity, Store } from "rc-field-form/lib/interface"; const layout = { labelCol: { span: 8, }, wrapperCol: { span: 16, },};const tailLayout = { wrapperCol: { offset: 8, span: 16, },};const App = () => { const onFinish = (values: Store) => { console.log("Success:", values); }; // const onFinishFailed = (errorInfo: Store) => { const onFinishFailed = (errorInfo: ValidateErrorEntity) => { console.log("Failed:", errorInfo); }; return ( <div className="login-page"> <Form {...layout} name="basic" initialValues={{ remember: true, }} onFinish={onFinish} onFinishFailed={onFinishFailed} > <Form.Item label="Username" name="username" rules={[ { required: true, message: "Please input your username!", }, ]} > <Input /> </Form.Item> <Form.Item label="Password" name="password" rules={[ { required: true, message: "Please input your password!", }, ]} > <Input.Password /> </Form.Item> <Form.Item {...tailLayout} name="remember" valuePropName="checked"> <Checkbox>Remember me</Checkbox> </Form.Item> <Form.Item {...tailLayout}> <Button type="primary" htmlType="submit"> Submit </Button> </Form.Item> </Form> </div> ); }; // ReactDOM.render(<Demo />, mountNode); export default App;
其中,onFinish 函數(shù)的 values 編輯器給我們報(bào)隱患提示,我們也無法確定 value 的類型,但是又不能填寫 any。因此,我們可以去找下 Form 中定義的類型。mac 用戶把鼠標(biāo)放在 import 中的 From 標(biāo)簽上( windows 用戶按住 cmd),進(jìn)入到源代碼中去,然后一直去查找我們的方法的定義,首先我們進(jìn)入到了:
然后 InternalForm 繼承了 InternalForm,我們再繼續(xù)去尋找,最后找到了源頭:
同理我們也可以找到 onFinishFailed:
最后在文件中引入這兩個(gè)類型即可。
經(jīng)過上面的測試之后,我們的項(xiàng)目基本上就算已經(jīng)搭建好了,接下來就可以繼續(xù)充實(shí)相關(guān)的頁面了。
這里再把文件整理下,把不需要的刪除,src 目錄下新建一個(gè) pages 的目錄,然后我們的頁面組件都放在這里,把 login 的代碼也在這個(gè)文件夾下新建一個(gè)文件存放,然后我們再修改下 App.ts:
import { Route, HashRouter, Switch } from "react-router-dom"; import React from "react"; import LoginPage from "./pages/login"; import Home from "./pages/home"; function App() { return ( <div> <HashRouter> <Switch> <Route path="/" exact component={Home}></Route> <Route path="/login" exact component={LoginPage}></Route> </Switch> </HashRouter> </div> );}export default App;
由于 react-router-dom 是 JS 編寫的文件,因此需要再安裝一個(gè)類型定義文件:
npm install @types/react-router-dom -D
到此,關(guān)于“有哪些關(guān)于TypeScript的知識點(diǎn)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。