TypeScript:重新發明一次 JavaScript

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:LeanCloud 工程師 王子亭"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲一個 Node.js 開發者,我很早便了解到了 TypeScript,但又因爲我對 CoffeeScript 的喜愛,直到 2016 年才試用了一下 TypeScript,但當時對它的學習並不深入,直到最近又在工作中用 TypeScript 開發了兩個後端項目,對 TypeScript 有了一些新的理解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"爲 JavaScript 添加類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家總會把 TypeScript 和其他語言去做對比,說它是在模仿 Java 或 C#,我也曾一度相信了這種說法。但其實並非如此,"},{"type":"text","marks":[{"type":"strong"}],"text":"TypeScript 的類型系統和工作機制是如此的獨特,無法簡單地描述成是在模仿哪一個語言,更像是在 JavaScript 的基礎上重新發明了 JavaScript"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"究其根本,TypeScript 並不是一個全新的語言,它是在一個已有的語言 —— 還是一個非常靈活的動態類型語言上添加靜態約束。在官方 Wiki 上的 "},{"type":"link","attrs":{"href":"https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals","title":""},"content":[{"type":"text","text":"TypeScript Design Goals"}]},{"type":"text","text":" 中有提到,TypeScript 並不是要從 JavaScript 中抽取出一個具有靜態化語義的子集,而是要儘可能去支持之前社區中已有的編程範式,避免與常見的用法產生不兼容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這意味着 TypeScript 試圖爲 JavaScript 已有的大量十分「動態」的特性去提供靜態語義。一般認爲「靜態類型」的標誌是在編譯時爲變量確定類型,但 TypeScript 很特殊,因爲 JavaScript 本身的動態性,TypeScript 中的類型更像是一種「約束」,它尊重已有的 JavaScript 設計範式,同時儘可能添加一點靜態約束 —— 這種約束不會影響到代碼的表達能力。或者說,TypeScript 會以 JavaScript 的表達能力爲先、以 JavaScript 的運行時行爲爲先,而靜態約束則次之。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣聽起來 TypeScript 是不是很無聊呢,畢竟 Python 也有 Type Checking,JavaScript 之前也有 Flow。的確如此,但 "},{"type":"text","marks":[{"type":"strong"}],"text":"TypeScript 的類型系統的表達能力和工具鏈的支持實在太強了,並不像其他一些靜態類型標註僅能覆蓋一些簡單的情況,而是能夠深刻地參與到整個開發過程中,提高開發效率"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面提到 TypeScript 並不想發明新的範式,而是要儘可能支持 JavaScript 已有的用法。因此雖然 TypeScript 有着強大的類型系統、大量的特性,但對於 JavaScript 開發者開說學習成本並不高,因爲幾乎每個特性都可以對應 JavaScript 社區中一種常見的範式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基於屬性的類型系統"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 JavaScript 中,對象(Object)是最常用的類型之一,我們會使用大量的對象字面量來組織數據,我們經常將很多不同的參數塞進一個對象,或者從一個函數中返回一個對象,對象中還可以再嵌套對象。可以說對象是 JavaScript 中最常用的數據容器,但並沒有類型去約束它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如 request 這個庫會要求使用者將發起請求的所有參數一股腦地以一個對象的形式作爲參數傳入。這就是非常典型的 JavaScript 風格。再比如 JavaScript 中一個 Promise 對象只需有 then 和 catch 這兩個實例方法就可以,而並不真的需要真的來自標準庫中的 Promise 構造器,實際上也有很多第三方的 Promise 的實現,或一些返回類 Promise 對象的庫(例如一些 ORM)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 JavaScript 中我們通常只關注一個對象是否有我們需要的屬性和方法,這種範式被稱爲「"},{"type":"link","attrs":{"href":"https://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B","title":""},"content":[{"type":"text","text":"鴨子類型"}]},{"type":"text","text":"(Duck typing)」,就是說「"},{"type":"text","marks":[{"type":"strong"}],"text":"當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱爲鴨子"},{"type":"text","text":"」。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以 TypeScript 選擇了一種基於屬性的類型系統(Structural type system),這種類型系統不再關注一個變量被標稱的類型(由哪一個構造器構造),而是 "},{"type":"text","marks":[{"type":"strong"}],"text":"在進行類型檢查時,將對象拆開,抽絲剝繭,逐個去比較組成這個對象的每一個不可細分的成員。如果一個對象有着一個類型所要求的所有屬性或方法,那麼就可以當作這個類型來使用"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是 TypeScript 類型系統的核心 —— Interface(接口):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"interface LabeledValue {\n label: string\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TypeScript 並不關心 Interface 本身的名字,與其說是「類型」,它更像是一種約束。一個對象只要有一個字符串類型的 label 屬性,就可以說它滿足了 LabeledValue 的約束。它可以是一個其他類的實例、可以是字面量、可以有額外的屬性;只要它滿足 LabeledValue 所要求的屬性,就可以被賦值給這個類型的變量、傳遞給這個類型的參數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面提到 Interface 實際上是一組屬性或一組約束的集合,說到集合,當然就可以進行交集、並集之類的運算。例如 "},{"type":"codeinline","content":[{"type":"text","text":"type C = A & B"}]},{"type":"text","text":" 表示 C 需要同時滿足類型 A 和類型 B 的約束,可以簡單地實現類型的組合;而 "},{"type":"codeinline","content":[{"type":"text","text":"type C = A | B"}]},{"type":"text","text":" 則表示 C 只需滿足 A 和 B 任一類型的約束,可以實現聯合類型(Union Type)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我會挑選一些 TypeScript 具有代表性的一些特性進行介紹,它們之間環環相扣,十分精妙。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"字符串魔法:字面量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 TypeScript 中,字面量也是一種類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"type Name = 'ziting'\nconst myName: Name = 'ziting'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的代碼中,Name 類型唯一合法的值就是 ziting 這個字符串 —— 這看起來毫無意義,但如果我們引入前面提到的集合運算(聯合類型)呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"type Method = 'GET' | 'PUT' | 'DELETE'\n\ninterface Request {\n method: Method\n url: string\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼中我們約束了 Request 的 method 只能是 GET、PUT 和 DELETE 之一,這比單純地約束它是一個字符串類型要更加準確。這是 JavaScript 開發者經常使用的一種模式 —— 用字符串來表示枚舉類型,字符串更靈活也更具有可讀性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 lodash 之類的庫中,JavaScript 開發者還非常喜歡使用字符串來傳遞屬性名,在 JavaScript 中這很容易出錯。而 TypeScript 則提供了專門的語法和內建的工具類型來實現對這些字符串字面量的計算,提供靜態的類型檢查:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"interface Todo {\n title: string\n description: string\n completed: boolean\n}\n\n// keyof 將 interface 的所有屬性名提取成一個新的聯合類型\ntype KeyOfTodo = keyof Todo // 'title' | 'description' | 'completed'\n// Pick 可以從一個 interface 中提取一組屬性,生成新的類型\ntype TodoPreview = Pick // {title: string, completed: boolean}\n// Extract 可以找到兩個並集類型的交集,生成新的類型\ntype Inter = Extract // 'title'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助這些語法和後面提到的泛型能力,JavaScript 中各種以字符串的形式傳遞屬性名、魔法般的對象處理,也都可以得到準確的類型檢查。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"類型元編程:泛型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"泛型提供了一種將類型參數化的能力,在其他語言中最基本的用途是定義容器類型,使得工具函數可以不必知道被操作的變量的具體類型。JavaScript 中的數組或 Promise 在 TypeScript 中都會被表述爲這樣的泛型類型,例如 Promise.all 的類型定義可以寫成:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"function all(values: Array>): Promise>"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到類型參數可以被用來構造更復雜的類型,進行集合運算或嵌套。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"默認情況下,因爲類型參數可以是任意的類型,所以不能假定它有某些屬性或方法,也就不能訪問它的任何屬性,只有添加了約束才能遵循這個約束去使用它,同時 TypeScript 會依照這個約束限制傳入的類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"interface Lengthwise {\n length: number\n}\n\nfunction logLength(arg: T) {\n console.log(arg.length)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"約束中也可以用到其他的類型參數或使用多個類型參數,在下面的代碼中我們限制類型參數 K 必須是 obj 的一個屬性名:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"function getProperty(obj: T, key: K) {\n return obj[key];\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了在函數上使用泛型之外,我們還可以定義泛型類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"type Partial = {\n [P in keyof T]?: T[P];\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當定義泛型類型時我們實際上是在定義一種處理類型的「函數」,使用泛型參數去生成新的類型,這也被稱作「元編程」。例如 Partial 會遍歷傳入類型 T 的每一個屬性,返回一個所有屬性都可空的新類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"interface Person {\n name: string\n}\n\nconst a: Person = {} // 報錯 Property 'name' is missing in type '{}' but required in type 'Person'.\nconst b: Partial = {}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面我們提到的 Pick 和 Extract 都是這樣的泛型類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此之外 TypeScript 甚至可以在定義泛型類型時進行條件判斷和遞歸,這使得 TypeScript 的類型系統變成了 "},{"type":"link","attrs":{"href":"https://github.com/microsoft/TypeScript/issues/14833","title":""},"content":[{"type":"text","text":"圖靈完備的"}]},{"type":"text","text":",可以在編譯階段進行任何計算。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能會懷疑這樣複雜的類型真的有用麼?其實這些特性更多地是提供給庫開發者使用的,對於 JavaScript 社區中的 ORM、數據結構,或者是 lodash 這樣的庫來說,如此強大的類型系統是非常必要的,lodash 的 "},{"type":"link","attrs":{"href":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash","title":""},"content":[{"type":"text","text":"類型定義"}]},{"type":"text","text":" 行數甚至是它本身代碼的幾十倍。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"類型方程式:自動推導"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但其實我們並不一定要掌握這麼複雜的類型系統,實際上前面介紹的高級特性在業務代碼中都極少被用到。TypeScript 並不希望標註類型給開發者造成太大的負擔,因此 TypeScript 會盡可能地進行類型推導,讓開發者在大多數情況下不必手動標註類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"const bool = true // bool 是 true(字面量類型)\nlet num = 1 // num 是 number\nlet arr = [0, 1, 'str'] // arr 是 (number | string)[]\n\nlet body = await fs.readFile() // body 是 Buffer\n\n// cpuModels 是 string[]\nlet cpuModels = os.cpus().map( cpu => {\n // cpu 是 os.CpuInfo\n return cpu.model\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類型推導同樣可以用在泛型中,例如前面提到的 Promise.all 和 getProperty,我們在使用時都不必去管泛型參數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"// 調用 Promise.all,files 的類型是 Promise\nconst files = Promise.all(paths.map( path => fs.readFile(path)))\n// 調用 Promise.all,numbers 的類型是 Promise\nconst numbers = Promise.all([1, 2, 3, 4])\n\n// 調用 getProperty,a 的類型是 number\nconst a = getProperty({a: 2}, 'a')"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面提到泛型是在將類型參數化,引入一個未知數來代替實際的類型,所以說泛型對於 TypeScript 就像是一個方程式一樣,只要你提供了能夠解開這個方程的其他未知數,TypeScript 就可以推導出剩餘的泛型類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"價值十億美金的錯誤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在很多語言中訪問空指針都會報出異常(在 JavaScript 中是從 null 或 undefined 上讀取屬性時),空指針異常被稱爲「"},{"type":"link","attrs":{"href":"https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/","title":""},"content":[{"type":"text","text":"價值十億美元的錯誤"}]},{"type":"text","text":"」。TypeScript 則爲空值檢查也提供了支持(需開啓 strictNullChecks),雖然這依賴於類型定義的正確性,並沒有運行時的保證,但依然可以提前在編譯期發現大部分的錯誤,提高開發效率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TypeScript 中的類型是不可爲空(undefined 或 null)的,對於可空的類型必須表示成和 undefined 或 null 的並集類型,這樣當你試圖從一個可能爲 undefined 的變量上讀取屬性時,TypeScript 就會報錯了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"function logDateValue1(date: Date) { // 參數不可空\n console.log(date.valueOf())\n}\n\nlogDateValue1(new Date)\nlogDateValue1() // 報錯 An argument for 'date' was not provided.\n\nfunction logDateValue2(date: Date | undefined) { // 參數可空\n console.log(date.valueOf()) // 報錯 Object is possibly 'undefined'.\n}\n\nlogDateValue2(new Date)\nlogDateValue2()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況下 TypeScript 會要求你先對這個值進行判斷,排除其爲 undefined 可能性。這就要說到 TypeScript 的另外一項特性 —— 其基於控制流的類型分析。例如在你使用 if 對變量進行非空判斷後,在 if 之後的花括號中這個變量就會變成非空類型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"function print(str: string | null) {\n // str 在這裏的類型是 string | null\n console.log(str.trim()) // 報錯 Object is possibly 'null'.\n if (str !== null) {\n // str 在這裏的類型是 string\n console.log(str.trim())\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的類型分析也發生在使用 if、switch 等語句對並集類型進行判斷時:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"ts"},"content":[{"type":"text","text":"interface Rectangle {\n kind: 'rectangle'\n width: number\n height: number\n}\n\ninterface Circle {\n kind: 'circle'\n radius: number\n}\n\nfunction area(s: Rectangle | Circle) {\n // s 在這裏的類型是 Rectangle | Circle\n switch (s.kind) {\n case 'rectangle':\n // s 在這裏的類型是 Rectangle\n return s.height * s.width\n case 'circle':\n // s 在這裏的類型是 Circle\n return Math.PI * s.radius ** 2;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"僅僅工作在編譯階段"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TypeScript 最終仍然會編譯到 JavaScript,再被 JavaScript 引擎(如 V8)執行,在生成出的代碼中不會包含任何類型信息,TypeScript 也不會添加任何與運行時行爲有關的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TypeScript 僅僅提供了類型檢查,但它並沒有去保證通過檢查的代碼一定是可以正確運行的。可能一個變量在 TypeScript 的類型聲明中是一個數字,但並不能阻止它在運行時變成一個字符串 —— 可能是使用了強制類型轉換或使用了其他非 TypeScript 的庫且類型定義文件有誤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 TypeScript 中你可以將類型設置爲 any 來繞過幾乎所有檢查,或者用 as 來強制「轉換」類型,當然就像前面提到的那樣,這裏轉換的僅僅是 TypeScript 在編譯階段的類型標註,並不會改變運行時的類型。雖然 TypeScript 設計上要去支持 JavaScript 的所有範式,但難免有一些極端的用例無法覆蓋到,這時如何使用 any 就非常考驗開發者的經驗了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"編程語言的類型系統總是需要在靈活和複雜、簡單和死板之間做出權衡,TypeScript 則給出了一個完全不同的答案 —— 將編譯期的檢查和運行時的行爲分別看待。這是 TypeScript 飽受爭議的一點,有人認爲這樣非常沒有安全感,即使通過了編譯期檢查在運行時依然有可能得到錯誤的類型,也有人認爲 "},{"type":"text","marks":[{"type":"strong"}],"text":"這是一個非常切合工程實際的選擇 —— 你可以用 any 來跳過類型檢查,添加一些過於複雜或無法實現的代碼,雖然這破壞了類型安全,但確實又解決了問題"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼這種僅僅工作在編譯階段類型檢查有意義麼?我認爲當然是有的,畢竟 JavaScript 已經提供了足夠使用的運行時行爲,而且要保持與 JavaScript 的互操作性。大家需要的只是 TypeScript 的類型檢查來提高開發效率,除了編譯階段的檢查來儘早發現錯誤以外,TypeScript 的類型信息也可以給編輯器(IDE)非常準確的補全建議。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"與 JavaScript 代碼一起工作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"任何基於 JavaScript 的技術都要去解決和標準 JavaScript 代碼的互操作性"},{"type":"text","text":" —— TypeScript 不可能創造出一個平行與 JavaScript 的世界,它必須依賴社區中已有的數十萬的 JavaScript 包。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此 TypeScript 引入了一種類型描述文件,允許社區爲 JavaScript 編寫類型描述文件,來讓用到它們的代碼可以得到 TypeScript 的類型檢查。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"描述文件的確是 TypeScript 開發中最大的痛點,畢竟只有當找全了定義文件之後,纔會有流暢的開發體驗。在開發的過程中不可避免地會用到一些特定領域的、小衆的庫,這時就必須要去考慮這個庫是否有定義文件、定義文件的質量如何、是否需要自己爲其編寫定義文件。對於不涉及複雜泛型的庫來說,寫定義文件並不會花太多時間,你也只需要給自己用到的接口寫定義,但終究是一個分心的點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"TypeScript 有着先進的類型系統,而且這個先進並不是「學術」意義上的先進,而是「工程」意義上的先進,能夠切實地提高開發效率,減輕動態類型的心理負擔,提前發現錯誤。所以在此建議所有的 JavaScript 開發者都瞭解和嘗試一下 TypeScript,對於 JavaScript 的開發者來說,TypeScript 的入門成本非常低。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 LeanCloud,控制檯在最近的一次的重構中切換到了 TypeScript,提高了前端項目的工程化水平,讓代碼可以被長時間地維護下去。同時我們一部分既有的基於 Node.js 的後端項目也在切換到 TypeScript。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LeanCloud 的一些內部工具和邊緣服務也會優先考慮 TypeScript,較低的學習成本(誰沒寫過幾行 JavaScript 呀!)、靜態類型檢查和優秀的 IDE 支持,極大地降低了新同事參與不熟悉或長時間無人維護的項目的門檻,提高大家改進內部工具的積極性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LeanCloud 的 JavaScript SDK、Node SDK 和 Play SDK 都添加了 TypeScript 的定義文件(並且打算在之後的版本中使用 TypeScript 改寫),讓使用 LeanCloud 的開發者可以在 TypeScript 中使用 SDK,即使不用 TypeScript,定義文件也可以幫助編輯器來改進代碼補全和類型提示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你也希望一起來完善這些項目,可以瞭解一下在 LeanCloud 的 "},{"type":"link","attrs":{"href":"https://www.leancloud.cn/jobs/","title":""},"content":[{"type":"text","text":"工作機會"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考資料:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://mariusschulz.com/blog/series/typescript-evolution","title":""},"content":[{"type":"text","text":"TypeScript Evolution"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://basarat.gitbook.io/typescript/","title":""},"content":[{"type":"text","text":"TypeScript Deep Dive"}]},{"type":"text","text":" ("},{"type":"link","attrs":{"href":"https://jkchao.github.io/typescript-book-chinese/","title":""},"content":[{"type":"text","text":"中文版"}]},{"type":"text","text":")"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals","title":""},"content":[{"type":"text","text":"TypeScript Design Goals"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.typescriptlang.org/docs/handbook/basic-types.html","title":""},"content":[{"type":"text","text":"The TypeScript Handbook"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://zhuanlan.zhihu.com/p/64446259","title":""},"content":[{"type":"text","text":"淺談 TypeScript 類型系統"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://zhuanlan.zhihu.com/p/85655537","title":""},"content":[{"type":"text","text":"TypeScript類型元編程:實現8位數的算術運算"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy","title":""},"content":[{"type":"text","text":"編程的智慧"}]},{"type":"text","text":"(正確處理 null 指針)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science/","title":""},"content":[{"type":"text","text":"The worst mistake of computer science"}]},{"type":"text","text":"("},{"type":"link","attrs":{"href":"https://www.open-open.com/news/view/16166e1","title":""},"content":[{"type":"text","text":"中文版"}]},{"type":"text","text":")"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章