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