你不知道的 TypeScript 高級技巧
高翔,微醫(yī)云服務(wù)團(tuán)隊前端工程師,喜歡美食,熱愛技術(shù),喜歡折騰。
前言
在 2020 年的今天,TS 已經(jīng)越來越火,不管是服務(wù)端(Node.js),還是前端框架(Angular、Vue3),都有越來越多的項目使用 TS 開發(fā),作為前端程序員,TS 已經(jīng)成為一項必不可少的技能,本文旨在介紹 TS 中的一些高級技巧,提高大家對這門語言更深層次的認(rèn)知。
Typescript 簡介
ECMAScript 的超集 (stage 3) 編譯期的類型檢查 不引入額外開銷(零依賴,不擴(kuò)展 js 語法,不侵入運行時) 編譯出通用的、易讀的 js 代碼
Typescript = Type + ECMAScript + Babel-Lite
Typescript 設(shè)計目標(biāo): https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
為什么使用 Typescript
增加了代碼的可讀性和可維護(hù)性 減少運行時錯誤,寫出的代碼更加安全,減少 BUG 享受到代碼提示帶來的好處 重構(gòu)神器
基礎(chǔ)類型
boolean number string array tuple enum void null & undefined any & unknown never
any 和 unknown 的區(qū)別
any: 任意類型unknown: 未知的類型
任何類型都能分配給 unknown,但 unknown 不能分配給其他基本類型,而 any 啥都能分配和被分配。
let?foo:?unknown
foo?=?true?//?ok
foo?=?123?//ok
foo.toFixed(2)?//?error
let?foo1:?string?=?foo?//?error
let?bar:?any
bar?=?true?//?ok
bar?=?123?//ok
foo.toFixed(2)?//?ok
let?bar1:string??=?bar?//?ok
可以看到,用了 any 就相當(dāng)于完全丟失了類型檢查,所以大家盡量少用 any,對于未知類型可以用 unknown。
unknown 的正確用法
我們可以通過不同的方式將 unknown 類型縮小為更具體的類型范圍:
function?getLen(value:?unknown):?number?{
??if?(typeof?value?===?'string')?{
????//?因為類型保護(hù)的原因,此處?value?被判斷為?string?類型
???return?value.length
??}
??
??return?0
}
這個過程叫類型收窄(type narrowing)。
never
never 一般表示哪些用戶無法達(dá)到的類型。在最新的 typescript 3.7 中,下面代碼會報錯:
//?never?用戶控制流分析
function?neverReach?():?never?{
??throw?new?Error('an?error')
}
const?x?=?2
neverReach()
x.toFixed(2)??//?x?is?unreachable
never 還可以用于聯(lián)合類型的 幺元:
type?T0?=?string?|?number?|?never?//?T0?is?string?|?number
函數(shù)類型
幾種函數(shù)類型的返回值類型寫法
function?fn():?number?{
??return?1
}
const?fn?=?function?():?number?{
??return?1
}
const?fn?=?():?number?=>?{
??return?1
}
const?obj?=?{
??fn?():?number?{
????return?1
??}
}
在
()后面添加返回值類型即可。
函數(shù)類型
ts 中也有函數(shù)類型,用來描述一個函數(shù):
type?FnType?=?(x:?number,?y:?number)?=>?number
完整的函數(shù)寫法
let?myAdd:?(x:?number,?y:?number)?=>?number?=?function(x:?number,?y:?number):?number?{
??return?x?+?y
}
//?使用?FnType?類型
let?myAdd:?FnType?=?function(x:?number,?y:?number):?number?{
??return?x?+?y
}
//?ts?自動推導(dǎo)參數(shù)類型
let?myAdd:?FnType?=?function(x,?y)?{
??return?x?+?y
}
函數(shù)重載?
js因為是動態(tài)類型,本身不需要支持重載,ts為了保證類型安全,支持了函數(shù)簽名的類型重載。即:
多個重載簽名和一個實現(xiàn)簽名
//?重載簽名(函數(shù)類型定義)
function?toString(x:?string):?string;
function?toString(x:?number):?string;
//?實現(xiàn)簽名(函數(shù)體具體實現(xiàn))
function?toString(x:?string?|?number)?{
??return?String(x)
}
let?a?=?toString('hello')?//?ok
let?b?=?toString(2)?//?ok
let?c?=?toString(true)?//?error
如果定義了重載簽名,則實現(xiàn)簽名對外不可見
function?toString(x:?string):?string;
function?toString(x:?number):?string?{
??return?String(x)
}
len(2)?//?error
實現(xiàn)簽名必須兼容重載簽名
function?toString(x:?string):?string;
function?toString(x:?number):?string;?//?error
//?函數(shù)實現(xiàn)
function?toString(x:?string)?{
??return?String(x)
}
重載簽名的類型不會合并
//?重載簽名(函數(shù)類型定義)
function?toString(x:?string):?string;
function?toString(x:?number):?string;
//?實現(xiàn)簽名(函數(shù)體具體實現(xiàn))
function?toString(x:?string?|?number)?{
??return?String(x)
}
function?stringOrNumber(x):?string?|?number?{
??return?x???''?:?0
}
//?input?是?string?和?number?的聯(lián)合類型
//?即?string?|?number
const?input?=?stringOrNumber(1)
toString('hello')?//?ok
toString(2)?//?ok
toString(input)?//?error
類型推斷
ts 中的類型推斷是非常強大,而且其內(nèi)部實現(xiàn)也是非常復(fù)雜的。
基本類型推斷:
//?ts?推導(dǎo)出?x?是?number?類型
let?x?=?10
對象類型推斷:
// ts 推斷出 myObj 的類型:myObj:?{ x: number; y: string; z: boolean;?}
const?myObj?=?{
??x:?1,
??y:?'2',
??z:?true
}
函數(shù)類型推斷:
//?ts?推導(dǎo)出函數(shù)返回值是?number?類型
function?len?(str:?string)?{
??return?str.length
}
上下文類型推斷:
//?ts?推導(dǎo)出?event?是?ProgressEvent?類型
const?xhr?=?new?XMLHttpRequest()
xhr.onload?=?function?(event)?{}
所以有時候?qū)τ谝恍┖唵蔚念愋涂梢圆挥檬謩勇暶髌漕愋停?ts 自己去推斷。
類型兼容性
typescript 的子類型是基于 結(jié)構(gòu)子類型 的,只要結(jié)構(gòu)可以兼容,就是子類型。(Duck Type)
class?Point?{
??x:?number
}
function?getPointX(point:?Point)?{
??return?point.x
}
class?Point2?{
??x:?number
}
let?point2?=?new?Point2()
getPointX(point2)?//?OK
java、c++ 等傳統(tǒng)靜態(tài)類型語言是基于 名義子類型 的,必須顯示聲明子類型關(guān)系(繼承),才可以兼容。
public?class?Main?{
??public?static?void?main?(String[]?args)?{
????getPointX(new?Point());?//?ok
????getPointX(new?ChildPoint());?//?ok
????getPointX(new?Point1());??//?error
??}
??public?static?void?getPointX?(Point?point)?{
????System.out.println(point.x);
??}
??static?class?Point?{
????public?int?x?=?1;
??}
??static?class?Point2?{
????public?int?x?=?2;
??}
????
??static?class?ChildPoint?extends?Point?{
????public?int?x?=?3;
??}
}
對象子類型
子類型中必須包含源類型所有的屬性和方法:
function?getPointX(point:?{?x:?number?})?{
??return?point.x
}
const?point?=?{
?x:?1,
??y:?'2'
}
getPointX(point)?//?OK
注意: 如果直接傳入一個對象字面量是會報錯的:
function?getPointX(point:?{?x:?number?})?{
??return?point.x
}
getPointX({?x:?1,?y:?'2'?})?//?error
這是 ts 中的另一個特性,叫做:? excess property check? ,當(dāng)傳入的參數(shù)是一個對象字面量時,會進(jìn)行額外屬性檢查。
函數(shù)子類型
介紹函數(shù)子類型前先介紹一下逆變與協(xié)變的概念,逆變與協(xié)變并不是 TS 中獨有的概念,在其他靜態(tài)語言中也有相關(guān)理念。
在介紹之前,先假設(shè)一個問題,約定如下標(biāo)記:
A?? B表示 A 是 B 的子類型,A 包含 B 的所有屬性和方法。A => B表示以 A 為參數(shù),B 為返回值的方法。(param: A) => B
如果我們現(xiàn)在有三個類型 Animal 、 Dog 、 WangCai(旺財) ,那么肯定存在下面的關(guān)系:
WangCai???Dog???Animal?//?即旺財屬于狗屬于動物
問題:以下哪種類型是 Dog => Dog 的子類呢?
WangCai => WangCaiWangCai => AnimalAnimal? => AnimalAnimal? => WangCai
從代碼來看解答
class?Animal?{
??sleep:?Function
}
class?Dog?extends?Animal?{
??//?吠
??bark:?Function
}
class?WangCai?extends?Dog?{
??dance:?Function
}
function?getDogName?(cb:?(dog:?Dog)?=>?Dog)?{
??const?dog?=?cb(new?Dog())
??dog.bark()
}
//?對于入?yún)碚f,WangCai 是 Dog 的子類,Dog 類上沒有 dance 方法, 產(chǎn)生異常。
//?對于出參來說,WangCai 類繼承了 Dog 類,肯定會有 bark 方法
getDogName((wangcai:?WangCai)?=>?{
??wangcai.dance()
??return?new?WangCai()
})
//?對于入?yún)碚f,WangCai 是 Dog 的子類,Dog 類上沒有 dance 方法, 產(chǎn)生異常。
//?對于出參來說,Animal 類上沒有 bark 方法, 產(chǎn)生異常。
getDogName((wangcai:?WangCai)?=>?{
??wangcai.dance()
??return?new?Animal()
})
//?對于入?yún)碚f,Animal 類是 Dog 的父類,Dog 類肯定有 sleep 方法。
//?對于出參來說,WangCai 類繼承了 Dog 類,肯定會有 bark 方法
getDogName((animal:?Animal)?=>?{
??animal.sleep()
??return?new?WangCai()
})
//?對于入?yún)碚f,Animal 類是 Dog 的父類,Dog 類肯定有 sleep 方法。
//?對于出參來說,Animal 類上沒有 bark 方法, 產(chǎn)生異常。
getDogName((animal:?Animal)?=>?{
??animal.sleep()
??return?new?Animal()
})
可以看到只有 Animal => WangCai 才是 Dog => Dog 的子類型,可以得到一個結(jié)論,對于函數(shù)類型來說,函數(shù)參數(shù)的類型兼容是反向的,我們稱之為 逆變 ,返回值的類型兼容是正向的,稱之為 協(xié)變 。
逆變與協(xié)變的例子只說明了函數(shù)參數(shù)只有一個時的情況,如果函數(shù)參數(shù)有多個時該如何區(qū)分?
其實函數(shù)的參數(shù)可以轉(zhuǎn)化為 Tuple 的類型兼容性:
type?Tuple1?=?[string,?number]
type?Tuple2?=?[string,?number,?boolean]
let?tuple1:?Tuple1?=?['1',?1]
let?tuple2:?Tuple2?=?['1',?1,?true]
let?t1:?Tuple1?=?tuple2?//?ok
let?t2:?Tuple2?=?tuple1?//?error
可以看到 Tuple2 => Tuple1 ,即長度大的是長度小的子類型,再由于函數(shù)參數(shù)的逆變特性,所以函數(shù)參數(shù)少的可以賦值給參數(shù)多的(參數(shù)從前往后需一一對應(yīng)),從數(shù)組的 forEach 方法就可以看出來:
[1,?2].forEach((item,?index)?=>?{
?console.log(item)
})?//?ok
[1,?2].forEach((item,?index,?arr,?other)?=>?{
?console.log(other)
})?//?error
高級類型
聯(lián)合類型與交叉類型
聯(lián)合類型(union type)表示多種類型的 “或” 關(guān)系
function?genLen(x:?string?|?any[])?{
??return?x.length
}
genLen('')?//?ok
genLen([])?//?ok
genLen(1)?//?error
交叉類型表示多種類型的 “與” 關(guān)系
interface?Person?{
??name:?string
??age:?number
}
interface?Animal?{
??name:?string
??color:?string
}
const?x:?Person?&?Animal?=?{
??name:?'x',
??age:?1,
??color:?'red
}
使用聯(lián)合類型表示枚舉
type?Position?=?'UP'?|?'DOWN'?|?'LEFT'?|?'RIGHT'
const?position:?Position?=?'UP'
可以避免使用
enum侵入了運行時。
類型保護(hù)
ts 初學(xué)者很容易寫出下面的代碼:
function?isString?(value)?{
??return?Object.prototype.toString.call(value)?===?'[object?String]'
}
function?fn?(x:?string?|?number)?{
??if?(isString(x))?{
????return?x.length?// error 類型“string | number”上不存在屬性“l(fā)ength”。
??}?else?{
????//?.....
??}
}
如何讓 ts 推斷出來上下文的類型呢?
1. 使用 ts 的 is 關(guān)鍵詞
function?isString?(value:?unknown):?value?is?string?{
??return?Object.prototype.toString.call(value)?===?'[object?String]'
}
function?fn?(x:?string?|?number)?{
??if?(isString(x))?{
????return?x.length
??}?else?{
????//?.....
??}
}
2. typeof 關(guān)鍵詞
在 ts 中,代碼實現(xiàn)中的 typeof 關(guān)鍵詞能夠幫助 ts 判斷出變量的基本類型:
function?fn?(x:?string?|?number)?{
??if?(typeof?x?===?'string')?{?//?x?is?string
????return?x.length
??}?else?{?//?x?is?number
????//?.....
??}
}
3. instanceof 關(guān)鍵詞
在 ts 中,instanceof 關(guān)鍵詞能夠幫助 ts 判斷出構(gòu)造函數(shù)的類型:
function?fn1?(x:?XMLHttpRequest?|?string)?{
??if?(x?instanceof?XMLHttpRequest)?{?//?x?is?XMLHttpRequest
????return?x.getAllResponseHeaders()
??}?else?{?//?x?is?string
????return?x.length
??}
}
4. 針對 null 和 undefined 的類型保護(hù)
在條件判斷中,ts 會自動對 null 和 undefined 進(jìn)行類型保護(hù):
function?fn2?(x?:?string)?{
??if?(x)?{
????return?x.length
??}
}
5. 針對 null 和 undefined 的類型斷言
如果我們已經(jīng)知道的參數(shù)不為空,可以使用 ! 來手動標(biāo)記:
function?fn2?(x?:?string)?{
??return?x!.length
}
typeof 關(guān)鍵詞
typeof 關(guān)鍵詞除了做類型保護(hù),還可以從實現(xiàn)推出類型,。
注意:此時的
typeof是一個類型關(guān)鍵詞,只可以用在類型語法中。
function?fn(x:?string)?{
??return?x.length
}
const?obj?=?{
??x:?1,
??y:?'2'
}
type?T0?=?typeof?fn?//?(x:?string)?=>?number
type?T1?=?typeof?obj?//?{x:?number;?y:?string?}
keyof 關(guān)鍵詞
keyof 也是一個 類型關(guān)鍵詞 ,可以用來取得一個對象接口的所有 key 值:
interface?Person?{
??name:?string
??age:?number
}
type?PersonAttrs?=?keyof?Person?//?'name'?|?'age'
in 關(guān)鍵詞
in 也是一個 類型關(guān)鍵詞, 可以對聯(lián)合類型進(jìn)行遍歷,只可以用在 type 關(guān)鍵詞下面。
type?Person?=?{
??[key?in?'name'?|?'age']:?number
}
//?{?name:?number;?age:?number;?}
[ ] 操作符
使用 [] 操作符可以進(jìn)行索引訪問,也是一個 類型關(guān)鍵詞
interface?Person?{
??name:?string
??age:?number
}
type?x?=?Person['name']?//?x?is?string
一個小栗子
寫一個類型復(fù)制的類型工具:
type?Copy?=?{
??[key?in?keyof?T]:?T[key]
}
interface?Person?{
??name:?string
??age:?number
}
type?Person1?=?Copy
泛型
泛型相當(dāng)于一個類型的參數(shù),在 ts 中,泛型可以用在 類、接口、方法、類型別名 等實體中。
小試牛刀
function?createList<T>():?T[]?{
??return?[]?as?T[]
}
const?numberList?=?createList<number>()?//?number[]
const?stringList?=?createList<string>()?//?string[]
有了泛型的支持,createList 方法可以傳入一個類型,返回有類型的數(shù)組,而不是一個
any[]。
泛型約束
如果我們只希望 createList 函數(shù)只能生成指定的類型數(shù)組,該如何做,可以使用 extends 關(guān)鍵詞來約束泛型的范圍和形狀。
type?Lengthwise?=?{
??length:?number
}
function?createList<T?extends?number?|?Lengthwise>():?T[]?{
??return?[]?as?T[]
}
const?numberList?=?createList<number>()?//?ok
const?stringList?=?createList<string>()?//?ok
const?arrayList?=?createList<any[]>()?//?ok
const?boolList?=?createList<boolean>()?//?error
any[]是一個數(shù)組類型,數(shù)組類型是有 length 屬性的,所以 ok。string類型也是有 length 屬性的,所以 ok。但是boolean就不能通過這個約束了。
條件控制
extends 除了做約束類型,還可以做條件控制,相當(dāng)于與一個三元運算符,只不過是針對 類型 的。
表達(dá)式:T extends U ? X : Y
含義:如果 T 可以被分配給 U,則返回 X,否則返回 Y。一般條件下,如果 T 是 U 的子類型,則認(rèn)為 T 可以分配給 U,例如:
type?IsNumber?=?T?extends?number???true?:?false
type?x?=?IsNumber<string>??//?false
映射類型
映射類型相當(dāng)于一個類型的函數(shù),可以做一些類型運算,輸入一個類型,輸出另一個類型,前文我們舉了個 Copy 的例子。
幾個內(nèi)置的映射類型
//?每一個屬性都變成可選
type?Partial?=?{
??[P?in?keyof?T]?:?T[P]
}
//?每一個屬性都變成只讀
type?Readonly?=?{
??readonly?[P?in?keyof?T]:?T[P]
}
//?選擇對象中的某些屬性
type?Pickextends?keyof?T>?=?{
??[P?in?K]:?T[P];
}
//?......
typescript 2.8 在 lib.d.ts 中內(nèi)置了幾個映射類型:
Partial-- 將T中的所有屬性變成可選。Readonly-- 將T中的所有屬性變成只讀。Pick-- 選擇T中可以賦值給U的類型。Exclude-- 從T中剔除可以賦值給U的類型。Extract-- 提取T中可以賦值給U的類型。NonNullable-- 從T中剔除null和undefined。ReturnType-- 獲取函數(shù)返回值類型。InstanceType-- 獲取構(gòu)造函數(shù)類型的實例類型。
所以我們平時寫 TS 時可以直接使用這些類型工具:
interface?ApiRes?{
??code:?string;
??flag:?string;
??message:?string;
??data:?object;
??success:?boolean;
??error:?boolean;
}
type?IApiRes?=?Pick'code'?|?'flag'?|?'message'?|?'data'>
//?{
//???code:?string;
//???flag:?string;
//???message:?string;
//???data:?object;
//?}
extends 條件分發(fā)
對于 T extends U ? X : Y 來說,還存在一個特性,當(dāng) T 是一個聯(lián)合類型時,會進(jìn)行條件分發(fā)。
type?Union?=?string?|?number
type?isNumber?=?T?extends?number???'isNumber'?:?'notNumber'
type?UnionType?=?isNumber?//?'notNumber'?|?'isNumber'
實際上,extends 運算會變成如下形式:
(string?extends?number???'isNumber'?:?'notNumber')?|?(number?extends?number???'isNumber'?:?'notNumber')
Extract 就是基于此特性,再配合 never 幺元的特性實現(xiàn)的:
type?Exclude?=?T?extends?K???never?:?T
type?T1?=?Exclude<string?|?number?|?boolean,?string?|?boolean>??//?number
infer 關(guān)鍵詞
infer 可以對運算過程中的類型進(jìn)行存儲,內(nèi)置的ReturnType 就是基于此特性實現(xiàn)的:
type?ReturnType?=?
??T?extends?(...args:?any)?=>?infer?R???R?:?never
type?Fn?=?(str:?string)?=>?number
type?FnReturn?=?ReturnType?//?number
模塊
全局模塊 vs. 文件模塊
默認(rèn)情況下,我們所寫的代碼是位于全局模塊下的:
const?foo?=?2
此時,如果我們創(chuàng)建了另一個文件,并寫下如下代碼,ts 認(rèn)為是正常的:
const?bar?=?foo?//?ok
如果要打破這種限制,只要文件中有 import 或者 export 表達(dá)式即可:
export?const?bar?=?foo?//?error
模塊解析策略
Tpescript 有兩種模塊的解析策略:Node 和 Classic。當(dāng) tsconfig.json 中 module 設(shè)置成 AMD、System、ES2015 時,默認(rèn)為 classic ,否則為 Node ,也可以使用 moduleResolution? 手動指定模塊解析策略。
兩種模塊解析策略的區(qū)別在于,對于下面模塊引入來說:
import?moduleB?from?'moduleB'
Classic 模式的路徑尋址:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
Node 模式的路徑尋址:
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json?(如果指定了"types"屬性)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json?(如果指定了"types"屬性)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json?(如果指定了"types"屬性)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
聲明文件
什么是聲明文件
聲明文件已 .d.ts 結(jié)尾,用來描述代碼結(jié)構(gòu),一般用來為 js 庫提供類型定義。
平時開發(fā)的時候有沒有這種經(jīng)歷:當(dāng)用npm安裝了某些包并使用的時候,會出現(xiàn)這個包的語法提示,下面是 vue 的提示:

這個語法提示就是聲明文件的功勞了,先來看一個簡單的聲明文件長啥樣,這是jsonp這個庫的聲明文件:
type?CancelFn?=?()?=>?void;
type?RequestCallback?=?(error:?Error?|?null,?data:?any)?=>?void;
interface?Options?{
????param?:?string;
????prefix?:?string;
????name?:?string;
????timeout?:?number;
}
declare?function?jsonp(url:?string,?options?:?Options,?cb?:?RequestCallback):?CancelFn;
declare?function?jsonp(url:?string,?callback?:?RequestCallback):?CancelFn;
export?=?jsonp;
有了這份聲明文件,編輯器在使用這個庫的時候就可以根據(jù)這份聲明文件來做出相應(yīng)的語法提示。
編輯器是怎么找到這個聲明文件?
如果這個包的根目錄下有一個 index.d.ts,那么這就是這個庫的聲明文件了。如果這個包的 package.json中有types或者typings字段,那個該字段指向的就是這個包的聲明文件。
上述兩種都是將聲明文件寫在包里面的情況,如果某個庫很長時間不維護(hù)了,或者作者消失了該怎么辦,沒關(guān)系,typescript官方提供了一個聲明文件倉庫,嘗試使用@types前綴來安裝某個庫的聲明文件:
npm?i?@types/lodash
當(dāng)引入lodash的時候,編輯器也會嘗試查找node_modules/@types/lodash 來為你提供lodash的語法提示。
還有一種就是自己寫聲明文件,編輯器會收集項目本地的聲明文件,如果某個包沒有聲明文件,你又想要語法提示,就可以自己在本地寫個聲明文件:
//?types/lodash.d.ts
declare?module?"lodash"?{
??export?function?chunk(array:?any[],?size?:?number):?any[];
??export?function?get(source:?any,?path:?string,?defaultValue?:?any):?any;
}

如果源代碼是用ts寫的,在編譯成js的時候,只要加上-d 參數(shù),就能生成對應(yīng)的聲明文件。
tsc?-d
聲明文件該怎么寫可以參考https://www.tslang.cn/docs/handbook/declaration-files/introduction.html
還要注意的是,如果某個庫有聲明文件了,編輯器就不會再關(guān)心這個庫具體的代碼了,它只會根據(jù)聲明文件來做提示。
擴(kuò)展原生對象
可能寫過 ts 的小伙伴有這樣的疑惑,我該如何在 window 對象上自定義屬性呢?
window.myprop?=?1?//?error
默認(rèn)的,window 上是不存在 myprop 這個屬性的,所以不可以直接賦值,當(dāng)然,可以使用方括號賦值語句,但是 get 操作時也必須用 [] ,并且沒有類型提示。
window['myprop']?=?1?//?OK
window.myprop??//?類型“Window?&?typeof?globalThis”上不存在屬性“myprop”
window['myprop']?//?ok,但是沒有提示,沒有類型
此時可以使用聲明文件擴(kuò)展其他對象,在項目中隨便建一個xxx.d.ts:
//?index.d.ts
interface?Window?{
??myprop:?number
}
//?index.ts
window.myprop?=?2??//?ok
也可以在模塊內(nèi)部擴(kuò)展全局對象:
import?A?from?'moduleA'
window.myprop?=?2
declare?global?{
??interface?Window?{
????myprop:?number
??}
}
擴(kuò)展其他模塊
如果使用過 ts 寫過 vue ?的同學(xué),一定都碰到過這個問題,如何擴(kuò)展 vue.prototype 上的屬性或者方法?
import?Vue?from?'vue'
Vue.prototype.myprops?=?1
const?vm?=?new?Vue({
??el:?'#app'
})
//?類型“CombinedVueInstance>”
//?上不存在屬性“myprops”
console.log(vm.myprops)
vue 給出的方案,在項目中的 xxx.d.ts 中擴(kuò)展 vue 實例上的屬性:
import?Vue?from?'vue'
declare?module?'vue/types/vue'?{
??interface?Vue?{
????myprop:?number
??}
}
ts 提供了 declare module 'xxx' 的語法來擴(kuò)展其他模塊,這非常有利于一些插件化的庫和包,例如 vue-router 擴(kuò)展 vue 。
//?vue-router/types/vue.d.ts
import?Vue?from?'vue'
import?VueRouter,?{?Route,?RawLocation,?NavigationGuard?}?from?'./index'
declare?module?'vue/types/vue'?{
??interface?Vue?{
????$router:?VueRouter
????$route:?Route
??}
}
declare?module?'vue/types/options'?{
??interface?ComponentOptionsextends?Vue>?{
????router?:?VueRouter
????beforeRouteEnter?:?NavigationGuard
????beforeRouteLeave?:?NavigationGuard
????beforeRouteUpdate?:?NavigationGuard
??}
}
如何處理非 js 文件,例如 .vue 文件引入?
處理 vue 文件
對于所有以 .vue 結(jié)尾的文件,可以默認(rèn)導(dǎo)出 Vue 類型,這是符合 vue單文件組件 的規(guī)則的。
declare?module?'*.vue'?{
??import?Vue?from?'vue'
??export?default?Vue
}
處理 css in js
對于所有的 .css,可以默認(rèn)導(dǎo)出一個 any 類型的值,這樣可以解決報錯問題,但是丟失了類型檢查。
declare?module?'*.css'?{
?const?content:?any
??export?default?content
}
import?*?as?React?from?'react'
import?*?as?styles?from?'./index.css'
const?Error?=?()?=>?(
????
?????????</div>
????????Ooooops!
p>
????????This?page?doesn't?exist?anymore.
????
)
export?default?Error
其實不管是全局?jǐn)U展還是模塊擴(kuò)展,其實都是基于 TS 聲明合并 的特性,簡單來說,TS 會將它收集到的一些同名的接口、類、類型別名按照一定的規(guī)則進(jìn)行合并。
編譯
ts 內(nèi)置了一個 compiler (tsc),可以讓我們把 ts 文件編譯成 js 文件,配合眾多的編譯選項,有時候不需要 babel? 我們就可以完成大多數(shù)工作。
常用的編譯選項
tsc 在編譯 ts 代碼的時候,會根據(jù) tsconfig.json 配置文件的選項采取不同的編譯策略。下面是三個常用的配置項:
target - 生成的代碼的JS語言的版本,比如ES3、ES5、ES2015等。 module - 生成的代碼所需要支持的模塊系統(tǒng),比如 es2015、commonjs、umd等。 lib - 告訴TS目標(biāo)環(huán)境中有哪些特性,比如 WebWorker、ES2015、DOM等。
和 babel 一樣,ts 在編譯的時候只會轉(zhuǎn)化新 語法,不會轉(zhuǎn)化新的 API, 所以有些場景下需要自行處理 polyfill 的問題。
更改編譯后的目錄
tsconfig 中的 outDir 字段可以配置編譯后的文件目錄,有利于 dist 的統(tǒng)一管理。
{
??"compilerOptions":?{
????"module":?"umd",
????"outDir":?"./dist"
??}
}
編譯后的目錄結(jié)構(gòu):
myproject
├──?dist
│???├──?index.js
│???└──?lib
│???????└──?moduleA.js
├──?index.ts
├──?lib
│???└──?moduleA.ts
└──?tsconfig.json
編譯后輸出到一個js文件中
對于 amd 和 system 模塊,可以配置 tsconfig.json 中的 outFile 字段,輸出為一個 js 文件。
如果需要輸出成其他模塊,例如 umd ,又希望打包成一個單獨的文件,需要怎么做?
可以使用 rollup 或者 webpack :
//?rollup.config.js
const?typescript?=?require('rollup-plugin-typescript2')
module.exports?=?{
??input:?'./index.ts',
??output:?{
????name:?'MyBundle',
????file:?'./dist/bundle.js',
????format:?'umd'
??},
??plugins:?[
????typescript()
??]
}
一些常用的 ts 周邊庫
@typescript-eslint/eslint-plugin、@typescript-eslint/parser - lint 套件 DefinitelyTyped - @types 倉庫 ts-loader、rollup-plugin-typescript2 - rollup、webpack 插件 typedoc - ts 項目自動生成 API 文檔 typeorm - 一個 ts 支持度非常高的、易用的數(shù)據(jù)庫 orm 庫 nest.js、egg.js - 支持 ts 的服務(wù)端框架 ts-node - node 端直接運行 ts 文件 utility-types - 一些實用的 ts 類型工具 type-coverage - 靜態(tài)類型覆蓋率檢測
一個提高開發(fā)效率的小技巧
大家在日常開發(fā)的時候,可能會經(jīng)常用到webpack的路徑別名,比如: import xxx from '@/path/to/name',如果編輯器不做任何配置的話,這樣寫會很尷尬,編譯器不會給你任何路徑提示,更不會給你語法提示。這里有個小技巧,基于 tsconfig.json 的 baseUrl和paths這兩個字段,配置好這兩個字段后,.ts文件里不但有了路徑提示,還會跟蹤到該路徑進(jìn)行語法提示。
這里有個小彩蛋,可以把 tsconfig.json 重命名成jsconfig.json,.js文件里也能享受到路徑別名提示和語法提示了。
使用 webstorm 的同學(xué)如果也想使用的話,只要打開設(shè)置,搜索webpack,然后設(shè)置一下webpack配置文件的路徑就好了。
學(xué)習(xí)推薦
Typescript 中文網(wǎng) Typescript 入門教程 github - awesome-typescript 知乎專欄 - 來玩TypeScript啊,機都給你開好了! conditional-types-in-typescript (ts 中的條件類型)
分享前端好文,點亮?在看?
