使用 Typescript 的一些注意事項(xiàng)(回顧總結(jié))

作者 @王福朋,原文地址:https://juejin.cn/post/6929793926979125255,作者授權(quán)轉(zhuǎn)載,如需轉(zhuǎn)載請聯(lián)系作者授權(quán)。
背景
ts 用了一年了,回顧起來,也沒有那么順利。趁這兩天春節(jié)假期有時(shí)間,整理了幾個(gè)自己覺得需要注意的情況,復(fù)盤一下。
我上學(xué)時(shí)學(xué)過 java 和 C# ,畢業(yè)后又做了兩年 C# 全棧開發(fā),對于靜態(tài)類型語言是有一定經(jīng)驗(yàn)的。ts 之所以能夠慢慢取代 js ,也是因?yàn)樗庆o態(tài)類型語言。
但 ts 和 java 是不一樣的,本質(zhì)是因?yàn)樗鳛橐粋€(gè)靜態(tài)類型語言,要編譯成弱類型語言 js 來執(zhí)行。所以,ts 只管得了編譯時(shí),卻管不了運(yùn)行時(shí)。 下文的很多內(nèi)容,都是這個(gè)特點(diǎn)的具體表現(xiàn)。
【個(gè)人提醒】我感覺 ts 為了能讓自己更適應(yīng) js 的轉(zhuǎn)型,做了很多非常繁瑣(或者叫靈活)的設(shè)計(jì),我沒有詳細(xì)總結(jié),但這種感覺很強(qiáng)烈。所以,如果你覺得 ts 有些地方過于繁瑣時(shí),也不要擔(dān)心,這可能不是你的問題,而是它的問題。
任何美好的東西,都是應(yīng)該簡單的、明確的。
易混亂的類型
如果問“ts 的變量有多少種類型”,你能否回答全面?ts 比 js 類型多一些。
never vs void
只需要記住一個(gè)特點(diǎn):返回 never 的函數(shù),都必須存在無法到達(dá)的終點(diǎn),如死循環(huán)、拋出異常。
function?fn1():?never?{
?while(true)?{?/*...*/?}
}
function?fn2():?never?{
?throw?new?Error(?/*...*/?)
}
any vs unknown
any任何類型,會忽略語法檢查unknown不可預(yù)知的類型,不會忽略語法檢查(這就是最大區(qū)別)
const?bar:?any?=?10;
any.substr(1);?//?OK?-?any?會忽略所有類型檢查
const?foo:?unknown?=?'string';
foo.substr(1);?//?Error:?語法檢查不通過報(bào)錯(cuò)
//?(foo?as?string).substr(1)?//?OK
//?if?(typeof?foo?===?'string')?{?foo.substr(1)?}?//?OK
一些“欺騙”編譯器語法檢查的行為
就如同你告訴編譯器:“按我寫的來,不要管太多,出了事兒我負(fù)責(zé)!”
編譯器不給你添麻煩了,不進(jìn)行語法檢查了,但你一定要考慮好后果。所以,以下內(nèi)容請慎用,不要無腦使用。
@ts-ignore
增加 @ts-ignore 的注釋,會忽略下一行的語法檢查。
const?num1:?number?=?100
num1.substr()?//?Error?語法檢查錯(cuò)誤
const?num2:?number?=?200
//?@ts-ignore
num2.substr()?//?Ok?語法檢查通過
any
如果 ts 是西游記,any 就是孫悟空,自由、無約束。了解西游記大部分是從孫悟空開始,了解 ts 可能也是從 any 開始用。
但西游記最后,孫悟空變成了佛。你的 any 也應(yīng)該變成 interface 或者 type 。
類型斷言 as
文章一開始說過,ts 只管編譯時(shí),不管運(yùn)行時(shí)。as 就是典型的例子,你用 as 告訴編譯器類型,編譯器就聽你的。但運(yùn)行時(shí),后果自負(fù)。
function?fn(a:?string?|?null):?void?{
????const?length?=?(a?as?string).length
????console.log(length)
}
fn('abc')?//?Ok
//?fn(null)?//?Error?js?運(yùn)行報(bào)錯(cuò)
非空斷言操作符 !
! 用于排除 null undefined ,即告訴編譯器:xx 變量肯定不是 null 或 undefined ,你放心吧~
同理,運(yùn)行時(shí)有可能出錯(cuò)。
//?例子?1
function?fn(a:?string?|?null?|?undefined)?{
????let?s:?string?=?''
????s?=?a?//?Error?語法檢查失敗
????s?=?a!?// OK ——?【注意】如果 a 真的是 null 或者 undefined ,那么 s 也會是 null 或者 undefined ,可能會帶來 bug ?。。?/span>
}
//?fn(null)
//?例子?2
type?NumGenerator?=?()?=>?number;
function?myFunc(numGenerator:?NumGenerator?|?undefined)?{
??const?num1?=?numGenerator();?//?Error?語法檢查失敗
??const?num2?=?numGenerator!();?//?OK
}
// myFunc(undefined)?//?【注意】,如果真的傳入 undefined ,也會去執(zhí)行,當(dāng)然會執(zhí)行報(bào)錯(cuò)?。?!
//?例子?3
let?a:?number
console.log(a)?//?Error?-?Variable?'n'?is?used?before?being?assigned.
let?b!:?number
console.log(b)?//?OK?-?`!`?表示,你會給?b?一個(gè)賦值,不用編譯器關(guān)心
可選鏈 ?.
?.遇到 null 或 undefined 就可以立即停止某些表達(dá)式的運(yùn)行,并返回 undefined【注意】這里只針對 null 和 undefined ,對于 0 false '' 等 falsely 變量是不起作用的。這一點(diǎn)和 && 不一樣。
這個(gè)運(yùn)算符,看似是獲取一個(gè)屬性,其實(shí)它是有條件判斷的。即,它就是一個(gè) ? : 三元表達(dá)式的語法糖。既然它有判斷邏輯,那你考慮不到位,就有可能出錯(cuò)。
//?例子?1?-?獲取對象屬性
interface?IFoo?{?a:?number?}
function?fn(obj:?IFoo?|?null?|?undefined):?number?|?undefined?{
????const?a?=?obj?.a?//??.?可選鏈運(yùn)算符
????//?第一,如果?a?是?IFoo?類型,則打印?100
????//?第二,如果?a?是?null?或者?undefined?,則打印?undefined
????console.log('a',?a)
????return?a?//?100?或者?undefined
}
fn({?a:?100?})
//?fn(null)
//?fn(undefined)
//?例子?2?-?獲取數(shù)組元素
function?tryGetArrayElement<T>(arr?:?T[],?index:?number?=?0)?{
??return?arr?.[index];
}
//?編譯產(chǎn)出:
//?"use?strict";
//?function?tryGetArrayElement(arr,?index?=?0)?{
//?????return?arr?===?null?||?arr?===?void?0???void?0?:?arr[index];
//?}
//?例子?3?-?用于函數(shù)調(diào)用
type?NumGenerator?=?()?=>?number;
function?fn(numGenerator:?NumGenerator?|?undefined?|?null)?{
??const?num?=?numGenerator?.();
??console.log('num',?num)?//?如果不是函數(shù),則不調(diào)用,也不會報(bào)錯(cuò),返回?undefined
}
//?fn(null)
//?fn(undefined)
【吐槽】對于這種語法糖,我還是比較反感的,我覺得自己寫幾行邏輯判斷會更好。它雖然簡潔,但是它會帶來閱讀理解上的負(fù)擔(dān),代碼簡潔不一定就可讀性好 —— 當(dāng)然了,如果大家都這么用,用久了,大家都熟悉了,可能也就沒有這個(gè)障礙了。
type 和 interface
關(guān)于兩者的區(qū)別,大家可以看看這篇文章 ,本文主要說一下我的理解。
先說結(jié)論:我目前還是處于一種懵逼狀態(tài)。我感覺 type 和 insterface 有太多的灰色地帶,這就導(dǎo)致我們?nèi)粘J褂脮r(shí),大部分情況下用誰都可以。我搞不懂 ts 為何要這樣設(shè)計(jì)。
按照我前些年對 java 和 C# 的理解:(我不知道近幾年 java C# 有沒有相關(guān)的語法變化)
如果自定義一個(gè)靜態(tài)的類型,僅有一些屬性,沒有方法,就用 type如果定義一種行為(行為肯定是需要方法的,僅屬性是不夠的),需要 class 實(shí)現(xiàn),就用 interface
但是查到的資料,以及查閱 ts 的類庫 lib.dom.d.ts 和 lib.es2015.d.ts 源碼,也都是用 interface 。我曾經(jīng)一度很困惑,見的多了,就慢慢習(xí)慣成自然了,但問題并沒有解決。
問題沒有解決,但事情還是要繼續(xù)做的,代碼也是要繼續(xù)寫的,所以我就一直跟隨大眾,盡量用 interface 。
private 和
兩者都表示私有屬性。背景不同:
private是 ts 中一開始就有的語法,而且目前只有 ts 有,ES 規(guī)范沒有。#是 ES 目前的提案語法,然后被 ts 3.8 支持了。即,ts 和 ES 都支持#。
如果僅對于 ts 來說,用哪個(gè)都一樣。
但本文一開始提到過:ts 只關(guān)注編譯時(shí),不關(guān)注運(yùn)行時(shí)。所以,還得看看兩者的編譯結(jié)果。
private
private 編譯之后,就失去了私有的特點(diǎn)。即,如果你執(zhí)行 (new Person()).name ,雖然語法檢查不通過,但運(yùn)行時(shí)是可以成功的。即,private 僅僅是 ts 的語法,編譯成 js 之后,就失效了。
//?ts?源碼
class?Person?{
????private?name:?string
????constructor()?{
????????this.name?=?'zhangsan'
????}
}
/*?編譯結(jié)果如下
"use?strict";
class?Person?{
????constructor()?{
????????this.name?=?'zhangsan';
????}
}
*/
#
# 編譯之后,依然具有私有特點(diǎn),而且用 (new Person()).name,在運(yùn)行時(shí)也是無法實(shí)現(xiàn)的。即,# 是 ts 語法,但同時(shí)也是 ES 的提案語法,編譯之后也不能失效。
但是,編譯結(jié)果中,“私有”是通過 WeekMap 來實(shí)現(xiàn)的,所以要確保你的運(yùn)行時(shí)環(huán)境支持 ES6 。WeekMap 沒有完美的 Polyfill 方案,強(qiáng)行 Polyfill 可能會發(fā)生內(nèi)存泄漏。
//?ts?源碼
class?Person?{
????#name:?string
????constructor()?{
????????this.#name?=?'zhangsan'
????}
}
/*?編譯結(jié)果如下
"use?strict";
var?__classPrivateFieldSet?=?(this?&&?this.__classPrivateFieldSet)?||?function?(receiver,?privateMap,?value)?{
????if?(!privateMap.has(receiver))?{
????????throw?new?TypeError("attempted?to?set?private?field?on?non-instance");
????}
????privateMap.set(receiver,?value);
????return?value;
};
var?_name;
class?Person?{
????constructor()?{
????????_name.set(this,?void?0);
????????__classPrivateFieldSet(this,?_name,?'zhangsan');
????}
}
_name?=?new?WeakMap();
*/
函數(shù)重載
java 中的函數(shù)重載
java 中的函數(shù)重載是非常好用,而且非常好理解的,傻瓜式的,一看就懂。如下代碼,定義了四個(gè)名為 test 的函數(shù),參數(shù)不同。那就直接寫四個(gè)函數(shù)即可,調(diào)用時(shí)也直接調(diào)用,java 會自動匹配。
public?class?Overloading?{
????public?int?test(){
????????System.out.println("test1");
????????return?1;
????}
????public?void?test(int?a){
????????System.out.println("test2");
????}???
????public?String?test(int?a,String?s){
????????System.out.println("test3");
????????return?"returntest3";
????}???
????public?String?test(String?s,int?a){
????????System.out.println("test4");
????????return?"returntest4";
????}???
????public?static?void?main(String[]?args){
????????Overloading?o?=?new?Overloading();
????????System.out.println(o.test());
????????o.test(1);
????????System.out.println(o.test(1,"test3"));
????????System.out.println(o.test("test4",1));
????}
}
ts 中的函數(shù)重載
ts 的函數(shù)重載,先把各個(gè)情況的函數(shù)頭寫出來,然后再寫一個(gè)統(tǒng)一的、兼容上述所有情況的函數(shù)頭。最后,函數(shù)體自行處理參數(shù)。
class?Person?{
????//?第一,各個(gè)情況的函數(shù)頭寫出來
????test():?void
????test(a:?number,?b:?number):?number
????test(a:?string,?b:?string):?string
????//?第二,統(tǒng)一的、兼容上述所有情況的函數(shù)頭(有一個(gè)不兼容,就報(bào)錯(cuò))
????test(a?:?string?|?number,?b?:?string?|?number):?void?|?string?|?number?{
????????//?第三,函數(shù)體自行處理參數(shù)
????????
????????if?(typeof?a?===?'string'?&&?typeof?b?===?'string')?{
????????????return?'string?params'
????????}
????????if?(typeof?a?===?'number'?&&?typeof?b?===?'number')?{
????????????return?'number?params'
????????}
????????console.log('no?params')
????}
}
這和 java 的語法比起來,簡直就是復(fù)雜 + 丑陋,完全違背設(shè)計(jì)原則。但是,為何要這樣呢?最終還是因?yàn)?ts 只關(guān)注編譯時(shí),管不了運(yùn)行時(shí) —— 這是原罪。試想,如果 ts 也設(shè)計(jì)像 java 一樣的重載寫法,那編譯出來的 js 代碼就會亂套的。因?yàn)?js 是弱類型的。
注意函數(shù)定義的順序
參數(shù)越精準(zhǔn)的,放在前面。
/*?錯(cuò)誤:any 類型不精準(zhǔn),應(yīng)該放在最后?*/
declare?function?fn(x:?any):?any;
declare?function?fn(x:?HTMLElement):?number;
declare?function?fn(x:?HTMLDivElement):?string;
var?myElem:?HTMLDivElement;
var?x?=?fn(myElem);?//?x:?any,?wat?
不要為僅在末尾參數(shù)不同時(shí)寫不同的重載,應(yīng)該盡可能使用可選參數(shù)。
/*?錯(cuò)誤?*/
interface?Example1?{
????diff(one:?string):?number;
????diff(one:?string,?two:?string):?number;
????diff(one:?string,?two:?string,?three:?boolean):?number;
}
/*?OK?*/
interface?Example2?{
????diff(one:?string,?two?:?string,?three?:?boolean):?number;
}
DOM 相關(guān)的類型
Vue 和 React 框架的普及,讓大部分業(yè)務(wù)開發(fā)者不用直接操作 DOM ,變成了框架工程師。但 Web 是基于 DOM 的,可以不用,但千萬不要忘記。
js 寫 DOM 操作非常簡單,不用關(guān)心類型,直接訪問屬性和方法即可。但用 ts 之后,就得關(guān)心 DOM 操作的相關(guān)類型。
不光我們使用 ts ,微軟在設(shè)計(jì) ts 時(shí),也需要定義 DOM 操作相關(guān)的類型,放在 ts 的類庫中,這樣 ts 才能被 web 場景所使用。這些都定義在 lib.dom.d.ts 中。補(bǔ):還有 ES 語法的內(nèi)置類庫,也在同目錄下。
PS:一門成熟可用的編程語言,最基本的要包括:語法 + 類庫 + 編譯器 + 運(yùn)行時(shí)(或者編譯器和運(yùn)行時(shí)統(tǒng)一為解釋器)。然后再說框架,工具,包管理器等這些外圍配置。
Node Element 等類型
這些都是現(xiàn)成的,W3C 早就定義好了的,我們直接回顧一下就可以。我覺得一張圖就可以很好的表達(dá),詳細(xì)的可以參考各自的 MDN 文檔。

事件參數(shù)類型
在使用 ts 之前,我并沒有特別關(guān)注事件參數(shù)類型(或者之前看過,后來不用,慢慢忘了),反正直接獲取屬性,拿來用就可以。
document.body.addEventListener('click',?e1?=>?{
????// e1 的構(gòu)造函數(shù)是什么?
})
document.body.addEventListener('keyup',?e2?=>?{
????// e2 的構(gòu)造函數(shù)是什么?
})
于是我查了一下 MDN 的文檔,其實(shí)也很好理解,就是不同的事件,參數(shù)類型是不一樣的,當(dāng)然屬性、方法也就不一樣。下面列出我們常見的,所有的類型參考 MDN 這個(gè)文檔。
| 事件 | 參數(shù)類型 |
|---|---|
| click dbclick mouseup mousedown mousemove mouseenter mouseleave | MouseEve |
| keyup keyrpess keydown | KeyboardEvent |
| compositionstart compositionupdate compositionend(輸入法) | CompositionEvent |
| focus blur focusin focusout | FocusEvent |
| drag drop | DragEvent |
| paste cut copy | ClipboardEvent |
他們的繼承關(guān)系如下圖。其中 UIEvent 表示的是用戶在 UI 觸發(fā)的一些事件。因?yàn)槭录粌H僅是用戶觸發(fā)的,還有 API 腳本觸發(fā)的,所以要單獨(dú)拿出一個(gè) UIEvent ,作為區(qū)分。

總結(jié)
我感覺重點(diǎn)的就是那句話:ts 是一門靜態(tài)類型語言,但它要編譯成為 js 這個(gè)弱類型語言來執(zhí)行,所以它管得了編譯時(shí),卻管不了運(yùn)行時(shí)。這是很多問題的根本。
目前看來,前端社區(qū)會慢慢往 ts 轉(zhuǎn)型,所以能熟練使用 ts 已經(jīng)是一名前端人員必備的技能。希望本文能給大家?guī)硪稽c(diǎn)點(diǎn)幫助。
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號
程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端?開發(fā)者,會討論?前端 Node 知識,互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
