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

這個(gè)語(yǔ)法提示就是聲明文件的功勞了,先來(lái)看一個(gè)簡(jiǎn)單的聲明文件長(zhǎng)啥樣,這是jsonp這個(gè)庫(kù)的聲明文件:
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;
有了這份聲明文件,編輯器在使用這個(gè)庫(kù)的時(shí)候就可以根據(jù)這份聲明文件來(lái)做出相應(yīng)的語(yǔ)法提示。
編輯器是怎么找到這個(gè)聲明文件?
如果這個(gè)包的根目錄下有一個(gè) index.d.ts,那么這就是這個(gè)庫(kù)的聲明文件了。如果這個(gè)包的 package.json中有types或者typings字段,那個(gè)該字段指向的就是這個(gè)包的聲明文件。
上述兩種都是將聲明文件寫(xiě)在包里面的情況,如果某個(gè)庫(kù)很長(zhǎng)時(shí)間不維護(hù)了,或者作者消失了該怎么辦,沒(méi)關(guān)系,typescript官方提供了一個(gè)聲明文件倉(cāng)庫(kù),嘗試使用@types前綴來(lái)安裝某個(gè)庫(kù)的聲明文件:
npm?i?@types/lodash
當(dāng)引入lodash的時(shí)候,編輯器也會(huì)嘗試查找node_modules/@types/lodash 來(lái)為你提供lodash的語(yǔ)法提示。
還有一種就是自己寫(xiě)聲明文件,編輯器會(huì)收集項(xiàng)目本地的聲明文件,如果某個(gè)包沒(méi)有聲明文件,你又想要語(yǔ)法提示,就可以自己在本地寫(xiě)個(gè)聲明文件:
//?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寫(xiě)的,在編譯成js的時(shí)候,只要加上-d 參數(shù),就能生成對(duì)應(yīng)的聲明文件。
tsc?-d
聲明文件該怎么寫(xiě)可以參考https://www.tslang.cn/docs/handbook/declaration-files/introduction.html
還要注意的是,如果某個(gè)庫(kù)有聲明文件了,編輯器就不會(huì)再關(guān)心這個(gè)庫(kù)具體的代碼了,它只會(huì)根據(jù)聲明文件來(lái)做提示。
擴(kuò)展原生對(duì)象
可能寫(xiě)過(guò) ts 的小伙伴有這樣的疑惑,我該如何在 window 對(duì)象上自定義屬性呢?
window.myprop?=?1?//?error
默認(rèn)的,window 上是不存在 myprop 這個(gè)屬性的,所以不可以直接賦值,當(dāng)然,可以使用方括號(hào)賦值語(yǔ)句,但是 get 操作時(shí)也必須用 [] ,并且沒(méi)有類(lèi)型提示。
window['myprop']?=?1?//?OK
window.myprop??//?類(lèi)型“Window?&?typeof?globalThis”上不存在屬性“myprop”
window['myprop']?//?ok,但是沒(méi)有提示,沒(méi)有類(lèi)型
此時(shí)可以使用聲明文件擴(kuò)展其他對(duì)象,在項(xiàng)目中隨便建一個(gè)xxx.d.ts:
//?index.d.ts
interface?Window?{
??myprop:?number
}
//?index.ts
window.myprop?=?2??//?ok
也可以在模塊內(nèi)部擴(kuò)展全局對(duì)象:
import?A?from?'moduleA'
window.myprop?=?2
declare?global?{
??interface?Window?{
????myprop:?number
??}
}
擴(kuò)展其他模塊
如果使用過(guò) ts 寫(xiě)過(guò) vue ?的同學(xué),一定都碰到過(guò)這個(gè)問(wèn)題,如何擴(kuò)展 vue.prototype 上的屬性或者方法?
import?Vue?from?'vue'
Vue.prototype.myprops?=?1
const?vm?=?new?Vue({
??el:?'#app'
})
//?類(lèi)型“CombinedVueInstance>”
//?上不存在屬性“myprops”
console.log(vm.myprops)
vue 給出的方案,在項(xiàng)目中的 xxx.d.ts 中擴(kuò)展 vue 實(shí)例上的屬性:
import?Vue?from?'vue'
declare?module?'vue/types/vue'?{
??interface?Vue?{
????myprop:?number
??}
}
ts 提供了 declare module 'xxx' 的語(yǔ)法來(lái)擴(kuò)展其他模塊,這非常有利于一些插件化的庫(kù)和包,例如 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 文件
對(duì)于所有以 .vue 結(jié)尾的文件,可以默認(rèn)導(dǎo)出 Vue 類(lèi)型,這是符合 vue單文件組件 的規(guī)則的。
declare?module?'*.vue'?{
??import?Vue?from?'vue'
??export?default?Vue
}
處理 css in js
對(duì)于所有的 .css,可以默認(rèn)導(dǎo)出一個(gè) any 類(lèi)型的值,這樣可以解決報(bào)錯(cuò)問(wèn)題,但是丟失了類(lèi)型檢查。
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
其實(shí)不管是全局?jǐn)U展還是模塊擴(kuò)展,其實(shí)都是基于 TS 聲明合并 的特性,簡(jiǎn)單來(lái)說(shuō),TS 會(huì)將它收集到的一些同名的接口、類(lèi)、類(lèi)型別名按照一定的規(guī)則進(jìn)行合并。
編譯
ts 內(nèi)置了一個(gè) compiler (tsc),可以讓我們把 ts 文件編譯成 js 文件,配合眾多的編譯選項(xiàng),有時(shí)候不需要 babel? 我們就可以完成大多數(shù)工作。
常用的編譯選項(xiàng)
tsc 在編譯 ts 代碼的時(shí)候,會(huì)根據(jù) tsconfig.json 配置文件的選項(xiàng)采取不同的編譯策略。下面是三個(gè)常用的配置項(xiàng):
target - 生成的代碼的JS語(yǔ)言的版本,比如ES3、ES5、ES2015等。 module - 生成的代碼所需要支持的模塊系統(tǒng),比如 es2015、commonjs、umd等。 lib - 告訴TS目標(biāo)環(huán)境中有哪些特性,比如 WebWorker、ES2015、DOM等。
和 babel 一樣,ts 在編譯的時(shí)候只會(huì)轉(zhuǎn)化新 語(yǔ)法,不會(huì)轉(zhuǎn)化新的 API, 所以有些場(chǎng)景下需要自行處理 polyfill 的問(wèn)題。
更改編譯后的目錄
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
編譯后輸出到一個(gè)js文件中
對(duì)于 amd 和 system 模塊,可以配置 tsconfig.json 中的 outFile 字段,輸出為一個(gè) js 文件。
如果需要輸出成其他模塊,例如 umd ,又希望打包成一個(gè)單獨(dú)的文件,需要怎么做?
可以使用 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 周邊庫(kù)
@typescript-eslint/eslint-plugin、@typescript-eslint/parser - lint 套件 DefinitelyTyped - @types 倉(cāng)庫(kù) ts-loader、rollup-plugin-typescript2 - rollup、webpack 插件 typedoc - ts 項(xiàng)目自動(dòng)生成 API 文檔 typeorm - 一個(gè) ts 支持度非常高的、易用的數(shù)據(jù)庫(kù) orm 庫(kù) nest.js、egg.js - 支持 ts 的服務(wù)端框架 ts-node - node 端直接運(yùn)行 ts 文件 utility-types - 一些實(shí)用的 ts 類(lèi)型工具 type-coverage - 靜態(tài)類(lèi)型覆蓋率檢測(cè)
一個(gè)提高開(kāi)發(fā)效率的小技巧
大家在日常開(kāi)發(fā)的時(shí)候,可能會(huì)經(jīng)常用到webpack的路徑別名,比如: import xxx from '@/path/to/name',如果編輯器不做任何配置的話(huà),這樣寫(xiě)會(huì)很尷尬,編譯器不會(huì)給你任何路徑提示,更不會(huì)給你語(yǔ)法提示。這里有個(gè)小技巧,基于 tsconfig.json 的 baseUrl和paths這兩個(gè)字段,配置好這兩個(gè)字段后,.ts文件里不但有了路徑提示,還會(huì)跟蹤到該路徑進(jìn)行語(yǔ)法提示。
這里有個(gè)小彩蛋,可以把 tsconfig.json 重命名成jsconfig.json,.js文件里也能享受到路徑別名提示和語(yǔ)法提示了。
使用 webstorm 的同學(xué)如果也想使用的話(huà),只要打開(kāi)設(shè)置,搜索webpack,然后設(shè)置一下webpack配置文件的路徑就好了。
學(xué)習(xí)推薦
Typescript 中文網(wǎng) Typescript 入門(mén)教程 github - awesome-typescript 知乎專(zhuān)欄 - 來(lái)玩TypeScript啊,機(jī)都給你開(kāi)好了! conditional-types-in-typescript (ts 中的條件類(lèi)型)
最后
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了
