使用TypeScript
已經有了一段時間,這的確是一個好東西,雖說在使用的過程中也發現了一些bug
,不過都是些小問題,所以整體體驗還是很不錯的。
TypeScript
之所以叫Type
,和它的強類型是分不開的,這也是區別於JavaScript
最關鍵的一點,類型的聲明可以直接寫在代碼中,也可以單獨寫一個用來表示類型的描述文件*.d.ts
。
常用方式
首先在d.ts
中是不會存在有一些簡單的基本類型定義的(因爲這些都是寫在表達式、變量後邊的,在這裏定義沒有任何意義),聲明文件中定義的往往都是一些複雜結構的類型。
大部分語法都與寫在普通ts
文件中的語法一致,也是export
後邊跟上要導出的成員。
最簡單的就是使用type
關鍵字來定義:
type A = { // 定義複雜結構
b: number
c: string
}
type Func = () => number // 定義函數
type Key = number | string // 多個類型
組合類型
以及在TypeScript
中有着很輕鬆的方式針對type
進行復用,比如我們有一個Animal
類型,以及一個Dog
類型,可以使用&
來進行復用。
P.S> &
符號可以拼接多個
type Animal = {
weight: number
height: number
}
type Dog = Animal & {
leg: number
}
動態的 JSON 類型指定
如果我們有一個JSON
結構,而它的key
是動態的,那麼我們肯定不能將所有的key
都寫在代碼中,我們只需要簡單的指定一個通配符即可:
type info = {
[k: string]: string | number // 可以指定多個類型
}
const infos: info = {
a: 1,
b: '2',
c: true, // error 類型不匹配
}
以及在新的版本中更推薦使用內置函數Record
來實現:
const infos: Record<number, string | number> = {
a: 1,
b: '2',
c: true, // error
}
獲取變量的類型
假如我們有一個JSON對象,裏邊包含了name
、age
兩個屬性,我們可以通過一些TypeScript
內置的工具函數來實現一些有意思的事情。
通過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
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 中使用函數重載
在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
來實現類似的功能,而非自定義的語法,所以就略過了這兩個關鍵字相關的描述
官方文檔中有針對如何編寫聲明文件的模版,可以參考:傳送陣