使用TypeScript
已經有了一段時間,這的確是一個好東西,雖然說在使用的過程當中也發現了一些bug
,不過都是些小問題,因此總體體驗仍是很不錯的。javascript
TypeScript
之因此叫Type
,和它的強類型是分不開的,這也是區別於JavaScript
最關鍵的一點,類型的聲明能夠直接寫在代碼中,也能夠單獨寫一個用來表示類型的描述文件*.d.ts
。html
首先在d.ts
中是不會存在有一些簡單的基本類型定義的(由於這些都是寫在表達式、變量後邊的,在這裏定義沒有任何意義),聲明文件中定義的每每都是一些複雜結構的類型。java
大部分語法都與寫在普通ts
文件中的語法一致,也是export
後邊跟上要導出的成員。typescript
最簡單的就是使用type
關鍵字來定義:json
type A = { // 定義複雜結構 b: number c: string } type Func = () => number // 定義函數 type Key = number | string // 多個類型
以及在TypeScript
中有着很輕鬆的方式針對type
進行復用,好比咱們有一個Animal
類型,以及一個Dog
類型,可使用&
來進行復用。app
P.S> &
符號能夠拼接多個函數
type Animal = { weight: number height: number } type Dog = Animal & { leg: number }
若是咱們有一個JSON
結構,而它的key
是動態的,那麼咱們確定不能將全部的key
都寫在代碼中,咱們只須要簡單的指定一個通配符便可:工具
type info = { [k: string]: string | number // 能夠指定多個類型 } const infos: info = { a: 1, b: '2', c: true, // error 類型不匹配 }
以及在新的版本中更推薦使用內置函數Record
來實現:ui
const infos: Record<string, string | number> = { a: 1, b: '2', c: true, // error }
假如咱們有一個JSON對象,裏邊包含了name
、age
兩個屬性,咱們能夠經過一些TypeScript
內置的工具函數來實現一些有意思的事情。this
經過keyof
與typeof
組合能夠獲得咱們想要的結果:
const obj = { name: 'Niko', age: 18 } // 若是是這樣的取值,只能寫在代碼中,不能寫在 d.ts 文件中,由於聲明文件裏邊不能存在實際有效的代碼 type keys = keyof typeof obj let a: keys = 'name' // pass let b: keys = 'age' // pass let c: keys = 'test' // error
而若是咱們想要將一個類型不統一的JSON
修改成統一類型的JSON
也可使用這種方式:
const obj = { name: 'Niko', age: 18, birthday: new Date() } const infos: Record<keyof typeof obj, string> = { name: '', age: '', birthday: 123, // 出錯,提示類型不匹配 test: '', // 提示不是`info`的已知類型 }
又好比說咱們有一個函數,函數會返回一個JSON
,而咱們須要這個JSON
來做爲類型。
那麼能夠經過ReturnType<>
來實現:
function func () { return { name: 'Niko', age: 18 } } type results = ReturnType<typeof func> // 或者也能夠拼接 keyof 獲取全部的 key type resultKeys = keyof ReturnType<typeof func> // 亦或者能夠放在`Object`中做爲動態的`key`存在 type infoJson = Record<keyof ReturnType<typeof func>, string>
class
類型由於咱們知道函數和class
在建立的時候是都有實際的代碼的(函數體、構造函數)。
可是咱們是寫在d.ts
聲明文件中的,這只是一個針對類型的約束,因此確定是不會存在真實的代碼的,可是若是在普通的ts
文件中這麼寫會出錯的,因此針對這類狀況,咱們須要使用declare
關鍵字,表示咱們這裏就是用來定義一個類型的,而非是一個對象、函數:
class Personal { name: string // ^ 出錯了,提示`name`必須顯式的進行初始化 } function getName (personal: Personal): name // ^ 出錯了,提示函數缺失實現
如下爲正確的使用方式:
-declare class Personal { +declare class Personal { name: string } -function getName (personal: Personal): name +declare function getName (personal: Personal): name
固然了,通常狀況下是不建議這麼定義class
的,應該使用interface
來代替它,這樣的class
應該僅存在於針對非TS
模塊的描述,若是是本身開發的模塊,那麼自己結構就具備聲明類型的特性。
這個概念是在一些強類型語言中才有的,依託於TypeScript
,這也算是一門強類型語言了,因此就會有須要用到這種聲明的地方。
例如咱們有一個add
函數,它能夠接收string
類型的參數進行拼接,也能夠接收number
類型的參數進行相加。
須要注意的是,只有在作第三方插件的函數重載定義時可以放到d.ts
文件中,其餘環境下建議將函數的定義與實現放在一塊兒(雖然說配置paths
也可以實現分開處理,可是那樣就失去了對函數建立時的約束)
// index.ts // 上邊是聲明 function add (arg1: string, arg2: string): string function add (arg1: number, arg2: number): number // 由於咱們在下邊有具體函數的實現,因此這裏並不須要添加 declare 關鍵字 // 下邊是實現 function add (arg1: string | number, arg2: string | number) { // 在實現上咱們要注意嚴格判斷兩個參數的類型是否相等,而不能簡單的寫一個 arg1 + arg2 if (typeof arg1 === 'string' && typeof arg2 === 'string') { return arg1 + arg2 } else if (typeof arg1 === 'number' && typeof arg2 === 'number') { return arg1 + arg2 } }
TypeScript
中的函數重載也只是多個函數的聲明,具體的邏輯還須要本身去寫,他並不會真的將你的多個重名 function 的函數體進行合併
想象一下,若是咱們有一個函數,傳入Date
類型的參數,返回其unix
時間戳,若是傳入Object
,則將對象的具體類型進行toString
輸出,其他狀況則直接返回,這樣的一個函數應該怎麼寫?
僅作示例演示,通常正常人不會寫出這樣的函數...
function build (arg: any) { if (arg instanceof Date) { return arg.valueOf() } else if (typeof arg === 'object') { return Object.prototype.toString.call(arg) } else { return arg } }
可是這樣的函數重載在聲明的順序上就頗有講究了,必定要將精確性高的放在前邊:
// 這樣是一個錯誤的示例,由於不管怎樣調用,返回值都會是`any`類型 function build(arg: any): any function build(arg: Object): string function build(arg: Date): number
由於TypeScript
在查找到一個函數重載的聲明之後就會中止不會繼續查找,any
是一個最模糊的範圍,而Object
又是包含Date
的,因此咱們應該按照順序從小到大進行排列:
function build(arg: Date): number function build(arg: Object): string function build(arg: any): any // 這樣在使用的時候才能獲得正確的類型提示 const res1 = build(new Date()) // number const res2 = build(() => { }) // string const res3 = build(true) // any
函數重載的意義在於可以讓你知道傳入不一樣的參數獲得不一樣的結果,若是傳入的參數不一樣,可是獲得的結果(類型)卻相同,那麼這裏就不要使用函數重載(沒有意義)。
若是函數的返回值類型相同,那麼就不須要使用函數重載
function func (a: number): number function func (a: number, b: number): number // 像這樣的是參數個數的區別,咱們可使用可選參數來代替函數重載的定義 function func (a: number, b?: number): number // 注意第二個參數在類型前邊多了一個`?` // 亦或是一些參數類型的區別致使的 function func (a: number): number function func (a: string): number // 這時咱們應該使用聯合類型來代替函數重載 function func (a: number | string): number
interface
是在TypeScript
中獨有的,在JavaScript
並無interface
一說。
由於interface
只是用來規定實現它的class
對應的行爲,沒有任何實質的代碼,對於腳本語言來講這是一個無效的操做
在語法上與class
並無什麼太大的區別,可是在interface
中只可以進行成員屬性的聲明,例如function
只可以寫具體接收的參數以及返回值的類型,並不可以在interface
中編寫具體的函數體,一樣的,針對成員屬性也不可以直接在interface
中進行賦值:
// 這是一個錯誤的示例 interface PersonalIntl { name: string = 'Niko' sayHi (): string { return this.name } } // 在 interface 中只能存在類型聲明 interface PersonalIntl { name: string sayHi (): string }
其實在一些狀況下使用interface
與普通的type
定義也沒有什麼區別。
好比咱們要導出一個存在name
和age
兩個屬性的對象:
// types/personal.d.ts export interface PersonalIntl { name: string age: number } // index.d.ts import { PersonalIntl } from './types/personal' const personal: PersonalIntl = { name: 'Niko', age: 18, }
若是將interface
換成type
定義也是徹底沒問題的:
// types/personal.d.ts export type PersonalIntl = { name: string age: number }
這樣的定義在基於上邊的使用是徹底沒有問題的,可是這樣也僅僅適用於Object
字面量的聲明,沒有辦法很好的約束class
模式下的使用,因此咱們採用interface
來約束class
的實現:
import { PersonalIntl } from './types/personal' class Personal implements PersonalIntl { constructor(public name: string, public age: number) { } // 上邊的簡寫與下述代碼效果一致 public name: string public age: number constructor (name: string, age: number) { this.name = name this.age = age } } const personal = new Personal('niko', 18)
首先,在接口中有兩種方式能夠定義一個函數,一個被定義在實例上,一個被定義在原型鏈上。
兩種聲明方式以下:
interface PersonalIntl { func1 (): any // 原型鏈方法 func2: () => any // 實例屬性 }
可是咱們在實現這兩個屬性時實際上是能夠互相轉換的,並無強要求必須使用哪一種方式:
class Personal implements PersonalIntl { func1 () { console.log(this) } func2 = () => { console.log(this) } }
其實這二者在編譯後的JavaScript
代碼中是有區別的,並不清楚這是一個bug
仍是設計就是如此,相似這樣的結構:
var Personal = /** @class */ (function () { function Personal() { var _this = this; this.func2 = function () { console.log(_this); }; } Personal.prototype.func1 = function () { console.log(this); }; return Personal; }());
因此在使用的時候仍是建議最好按照interface
定義的方式來建立,避免一些可能存在的奇奇怪怪的問題。
由於interface
是TypeScript
特有的,因此也會有一些有意思的特性,好比相同命名的interface
會被自動合併:
interface PersonalIntl { name: string } interface PersonalIntl { age: number } class Personal implements PersonalIntl { name = 'Niko' age = 18 }
在interface
中使用函數重載,你會獲得一個錯誤的結果,仍是拿上邊的build
函數來講,若是在interface
中聲明,而後在class
中實現,那麼不管怎樣調用,返回值的類型都會認爲是any
。
因此正確的作法是在class
中聲明重載,在class
中實現,interface
中最多隻定義一個any
,而非三個重載。
class Util implements UtilIntl { build(arg: Date): number build(arg: Object): string build(arg: any): any build(arg: any) { if (arg instanceof Date) { return arg.valueOf() } else if (typeof arg === 'object') { return Object.prototype.toString.call(arg) } else { return arg } } }
有關TypeScript
聲明類型聲明相關的目前就總結了這些比較經常使用的,歡迎小夥伴們進行補充。
在以前的版本中有存在module
和namespace
的定義,可是目前來看,好像更推薦使用 ES-Modules 版本的 import
/export
來實現相似的功能,而非自定義的語法,因此就略過了這兩個關鍵字相關的描述
官方文檔中有針對如何編寫聲明文件的模版,能夠參考:傳送陣