使用 Typescript 的一些注意事項(xiàng)
背景
我上學(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
?任何類型,會(huì)忽略語法檢查unknown
?不可預(yù)知的類型,不會(huì)忽略語法檢查(這就是最大區(qū)別)
const bar: any = 10;any.substr(1); // OK - any 會(huì)忽略所有類型檢查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
?的注釋,會(huì)忽略下一行的語法檢查。
const num1: number = 100num1.substr() // Error 語法檢查錯(cuò)誤const num2: number = 200// @ts-ignorenum2.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ò)。
// 例子 1function fn(a: string | null | undefined) {
? ?let s: string = ''
? ?s = a // Error 語法檢查失敗
? ?s = a! // OK —— 【注意】如果 a 真的是 null 或者 undefined ,那么 s 也會(huì)是 null 或者 undefined ,可能會(huì)帶來 bug ?。?!}// fn(null)
// 例子 2type NumGenerator = () => number;function myFunc(numGenerator: NumGenerator | undefined) {
?const num1 = numGenerator(); // Error 語法檢查失敗
?const num2 = numGenerator!(); // OK}// myFunc(undefined) // 【注意】,如果真的傳入 undefined ,也會(huì)去執(zhí)行,當(dāng)然會(huì)執(zhí)行報(bào)錯(cuò)?。?!
// 例子 3let a: numberconsole.log(a) // Error - Variable 'n' is used before being assigned.let b!: numberconsole.log(b) // OK - `!` 表示,你會(huì)給 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)用,也不會(huì)報(bào)錯(cuò),返回 undefined}// fn(null)// fn(undefined)
【吐槽】對于這種語法糖,我還是比較反感的,我覺得自己寫幾行邏輯判斷會(huì)更好。它雖然簡潔,但是它會(huì)帶來閱讀理解上的負(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 可能會(huì)發(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 會(huì)自動(dòng)匹配。
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 代碼就會(huì)亂套的。因?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 mouseleaveMouseEventkeyup keyrpess keydownKeyboardEventcompositionstart compositionupdate compositionend(輸入法)CompositionEventfocus blur focusin focusoutFocusEventdrag dropDragEventpaste cut copyClipboardEvent
他們的繼承關(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ū)會(huì)慢慢往 ts 轉(zhuǎn)型,所以能熟練使用 ts 已經(jīng)是一名前端人員必備的技能。希望本文能給大家?guī)硪稽c(diǎn)點(diǎn)幫助。

相關(guān)視頻傳送門:
https://www.bilibili.com/video/BV1sU4y1a7ri
https://www.bilibili.com/video/BV1WK4y1J7fs
作者:雙越
鏈接:https://www.imooc.com/article/315202
來源:慕課網(wǎng)
本文原創(chuàng)發(fā)布于慕課網(wǎng) ,轉(zhuǎn)載請注明出處,謝謝合作