精讀原文是 typescript 2.0-2.9 的文檔:html
我發現,許多寫了一年以上 Typescript 開發者,對 Typescript 對理解和使用水平都停留在入門階段。形成這個現象的緣由是,Typescript 知識的積累須要 刻意練習,使用 Typescript 的時間與對它的瞭解程度幾乎沒有關係。vue
這篇文章精選了 TS 在 2.0-2.9
版本中最重要的功能,並配合實際案例解讀,幫助你快速跟上 TS 的更新節奏。webpack
對於 TS 內部優化的用戶無感部分並不會羅列出來,由於這些優化均可在平常使用過程當中感覺到。git
因爲 Typescript 在嚴格模式下的許多表現都與非嚴格模式不一樣,爲了不沒必要要的記憶,建議只記嚴格模式就行了!github
直接訪問一個變量的屬性時,若是這個變量是 undefined
,不但屬性訪問不到,js 還會拋出異常,這幾乎是業務開發中最高頻的報錯了(每每是後端數據異常致使的),而 typescript 的 strict
模式會檢查這種狀況,不容許不安全的代碼出現。web
在 2.0
版本,提供了 「非空斷言標誌符」 !.
解決明確不會報錯的狀況,好比配置文件是靜態的,那確定不會拋出異常,但在 2.0
以前的版本,咱們可能要這麼調用對象:typescript
const config = { port: 8000 }; if (config) { console.log(config.port); }
有了 2.0
提供的 「非空斷言標誌符」,咱們能夠這麼寫了:npm
console.log(config!.port);
在 2.8
版本,ts 支持了條件類型語法:json
type TypeName<T> = T extends string ? "string"
當 T 的類型是 string 時,TypeName 的表達式類型爲 "string"。
這這時能夠構造一個自動 「非空斷言」 的類型,把代碼簡化爲:
console.log(config.port);
前提是框架先把 config
指定爲這個特殊類型,這個特殊類型的定義以下:
export type PowerPartial<T> = { [U in keyof T]?: T[U] extends object ? PowerPartial<T[U]> : T[U] };
也就是 2.8
的條件類型容許咱們在類型判斷進行遞歸,把全部對象的 key 都包一層 「非空斷言」!
此處靈感來自 egg-ts 總結
never
object
類型當一個函數沒法執行完,或者理解爲中途中斷時,TS 2.0
認爲它是 never
類型。
好比 throw Error
或者 while(true)
都會致使函數返回值類型時 never
。
和 null
undefined
特性同樣,never
等因而函數返回值中的 null
或 undefined
。它們都是子類型,好比類型 number
自帶了 null
與 undefined
這兩個子類型,是由於任何有類型的值都有多是空(也就是執行期間可能沒有值)。
這裏涉及到很重要的概念,就是預約義了類型不表明類型必定如預期,就比如函數運行時可能由於 throw Error
而中斷。因此 ts 爲了處理這種狀況,將 null
undefined
設定爲了全部類型的子類型,而從 2.0
開始,函數的返回值類型又多了一種子類型 never
。
TS 2.2
支持了 object
類型, 但許多時候咱們總把 object
與 any
類型弄混淆,好比下面的代碼:
const persion: object = { age: 5 }; console.log(persion.age); // Error: Property 'age' does not exist on type 'object'.
這時候報錯會出現,有時候閉個眼改爲 any
就完事了。其實這時候只要把 object
刪掉,換成 TS 的自動推導就搞定了。那麼問題出在哪裏?
首先 object
不是這麼用的,它是 TS 2.3
版本中加入的,用來描述一種非基礎類型,因此通常用在類型校驗上,好比做爲參數類型。若是參數類型是 object
,那麼容許任何對象數據傳入,但不容許 3
"abc"
這種非對象類型:
declare function create(o: object | null): void; create({ prop: 0 }); // 正確 create(null); // 正確 create(42); // 錯誤 create("string"); // 錯誤 create(false); // 錯誤 create(undefined); // 錯誤
而一開始 const persion: object
這種用法,是將能精確推導的對象類型,擴大到了總體的,模糊的對象類型,TS 天然沒法推斷這個對象擁有哪些 key
,由於對象類型僅表示它是一個對象類型,在將對象做爲總體觀察時是成立的,可是 object
類型是不認可任何具體的 key
的。
TS 在 2.0
版本支持了 readonly
修飾符,被它修飾的變量沒法被修改。
在 TS 2.8
版本,又增長了 -
與 +
修飾修飾符,有點像副詞做用於形容詞。舉個例子,readonly
就是 +readonly
,咱們也可使用 -readonly
移除只讀的特性;也能夠經過 -?:
的方式移除可選類型,所以能夠延伸出一種新類型:Required<T>
,將對象全部可選修飾移除,天然就成爲了必選類型:
type Required<T> = { [P in keyof T]-?: T[P] };
也是 TS 2.0
版本中,咱們能夠定製 this
的類型,這個在 vue
框架中尤其有用:
function f(this: void) { // make sure `this` is unusable in this standalone function }
this
類型是一種假參數,因此並不會影響函數真正參數數量與位置,只不過它定義在參數位置上,並且永遠會插隊在第一個。
簡單來講,就是模塊名能夠用 *
表示任何單詞了:
declare module "*!text" { const content: string; export default content; }
它的類型能夠輻射到:
import fileContent from "./xyz.txt!text";
這個特性很強大的一個點是用在拓展模塊上,由於包括 tsconfig.json
的模塊查找也支持通配符了!舉個例子一下就懂:
最近比較火的 umi
框架,它有一個 locale
插件,只要安裝了這個插件,就能夠從 umi/locale
獲取國際化內容:
import { locale } from "umi/locale";
其實它的實現是建立了一個文件,經過 webpack.alias
將引用指了過去。這個作法很是棒,那麼如何爲它加上類型支持呢?只要這麼配置 tsconfig.json
:
{ "compilerOptions": { "paths": { "umi/*": ["umi", "<somePath>"] } } }
將全部 umi/*
的類型都指向 <somePath>
,那麼 umi/locale
就會指向 <somePath>/locale.ts
這個文件,若是插件自動建立的文件名也剛好叫 locale.ts
,那麼類型就自動對應上了。
TS 在 2.x
支持了許多新 compileOptions
,但 skipLibCheck
實在是太耀眼了,筆者必須單獨提出來講。
skipLibCheck
這個屬性不但能夠忽略 npm 不規範帶來的報錯,還能最大限度的支持類型系統,可謂一箭雙鵰。
拿某 UI 庫舉例,某天發佈的小版本 d.ts
文件出現一個漏洞,致使整個項目構建失敗,你再也不須要提 PR 催促做者修復了!skipLibCheck
能夠忽略這種報錯,同時還能保持類型的自動推導,也就是說這比 declare module "ui-lib"
將類型設置爲 any
更強大。
TS 2.1
版本可謂是針對類型操做革命性的版本,咱們能夠經過 keyof
拿到對象 key 的類型:
interface Person { name: string; age: number; } type K1 = keyof Person; // "name" | "age"
基於 keyof
,咱們能夠加強對象的類型:
type NewObjType<T> = { [P in keyof T]: T[P] };
Tips:在 TS 2.8
版本,咱們能夠以表達式做爲 keyof
的參數,好比 keyof (A & B)
。
Tips:在 TS 2.9
版本,keyof
可能返回非 string
類型的值,所以從一開始就不要認爲 keyof
的返回類型必定是 string
。
NewObjType
原封不動的將對象類型從新描述了一遍,這看上去沒什麼意義。但實際上咱們有三處拓展的地方:
readonly
修飾,將對象的屬性變成只讀。:
改爲 ?:
,將對象全部屬性變成可選。Promise<T[P]>
,將對象每一個 key
的 value
類型覆蓋。基於這些能力,咱們拓展出一系列上層頗有用的 interface
:
2.8
的條件類型語法,實現遞歸設置只讀。"name" | "age"
就能夠生成僅支持這兩個 key 的新對象類型。2.8
版本才內置進來,能夠認爲 Pick 是挑選對象的某些 key,Extract 是挑選 key 中的 key。Record
接口才能完成推導。type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
T
的 null
與 undefined
的可能性。T
返回值的類型,這個類型意義很大。以上類型都內置在 lib.d.ts 中,不須要定義就可直接使用,能夠認爲是 Typescript 的 utils 工具庫。
單獨拿 ReturnType
舉個例子,體現出其重要性:
Redux 的 Connect 第一個參數是 mapStateToProps
,這些 Props 會自動與 React Props 聚合,咱們能夠利用 ReturnType<typeof currentMapStateToProps>
拿到當前 Connect 注入給 Props 的類型,就能夠打通 Connect 與 React 組件的類型系統了。
TS 2.3
版本作了許多對 Generators 的加強,但實際上咱們早已用 async/await 替代了它,因此 TS 對 Generators 的加強能夠忽略。須要注意的一塊是對 for..of
語法的異步迭代支持:
async function f() { for await (const x of fn1()) { console.log(x); } }
這能夠對每一步進行異步迭代。注意對比下面的寫法:
async function f() { for (const x of await fn2()) { console.log(x); } }
對於 fn1
,它的返回值是可迭代的對象,而且每一個 item 類型都是 Promise 或者 Generator。對於 fn2
,它自身是個異步函數,返回值是可迭代的,並且每一個 item 都不是異步的。舉個例子:
function fn1() { return [Promise.resolve(1), Promise.resolve(2)]; } function fn2() { return [1, 2]; }
在這裏順帶一提,對 Array.map
的每一項進行異步等待的方法:
await Promise.all( arr.map(async item => { return await item.run(); }) );
若是爲了執行順序,能夠換成 for..of
的語法,由於數組類型是一種可迭代類型。
瞭解這個以前,先介紹一下 TS 2.0
以前就支持的函數類型重載。
首先 JS 是不支持方法重載的,Java 是支持的,而 TS 類型系統必定程度在對標 Java,固然要支持這個功能。好在 JS 有一些偏方實現僞方法重載,典型的是 redux 的 createStore
:
export default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState; preloadedState = undefined; } }
既然 JS 有辦法支持方法重載,那 TS 補充了函數類型重載,二者結合就等於 Java 方法重載:
declare function createStore( reducer: Reducer, preloadedState: PreloadedState, enhancer: Enhancer ); declare function createStore(reducer: Reducer, enhancer: Enhancer);
能夠清晰的看到,createStore
想表現的是對參數個數的重載,若是定義了函數類型重載,TS 會根據函數類型自動判斷對應的是哪一個定義。
而在 TS 2.3
版本支持了泛型默認參數,能夠某些場景減小函數類型重載的代碼量,好比對於下面的代碼:
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>; declare function create<T extends HTMLElement>(element: T): Container<T, T[]>; declare function create<T extends HTMLElement, U extends HTMLElement>( element: T, children: U[] ): Container<T, U[]>;
經過枚舉表達了範型默認值,以及 U 與 T 之間可能存在的關係,這些均可以用泛型默認參數解決:
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>( element?: T, children?: U ): Container<T, U>;
尤爲在 React 使用過程當中,若是用泛型默認值定義了 Component
:
.. Component<Props = {}, State = {}> ..
就能夠實現如下等價的效果:
class Component extends React.PureComponent<any, any> { //... } // 等價於 class Component extends React.PureComponent { //... }
TS 從 2.4
版本開始支持了動態 Import,同時 Webpack4.0 也支持了這個語法(在 精讀《webpack4.0%20 升級指南》 有詳細介紹),這個語法就正式能夠用於生產環境了:
const zipUtil = await import("./utils/create-zip-file");
準確的說,動態 Import 實現於 webpack 2.1.0-beta.28,最終在 TS
2.4
版本得到了語法支持。
在 TS 2.9
版本開始,支持了 import()
類型定義:
const zipUtil: typeof import('./utils/create-zip-file') = await import('./utils/create-zip-file')
也就是 typeof
能夠做用於 import()
語法,而不真正引入 js 內容。不過要注意的是,這個 import('./utils/create-zip-file')
路徑須要可被推導,好比要存在這個 npm 模塊、相對路徑、或者在 tsconfig.json
定義了 paths
。
好在 import
語法自己限制了路徑必須是字面量,使得自動推導的成功率很是高,只要是正確的代碼幾乎必定能夠推導出來。好吧,因此這也從另外一個角度推薦你們放棄 require
。
從 Typescript 2.4
開始,支持了枚舉類型使用字符串作爲 value:
enum Colors { Red = "RED", Green = "GREEN", Blue = "BLUE" }
筆者在這提醒一句,這個功能在純前端代碼內可能沒有用。由於在 TS 中全部 enum
的地方都建議使用 enum
接收,下面給出例子:
// 正確 { type: monaco.languages.types.Folder; } // 錯誤 { type: 75; }
不只是可讀性,enum
對應的數字可能會改變,直接寫 75
的作法存在風險。
但若是先後端存在交互,前端是不可能發送 enum
對象的,必需要轉化成數字,這時使用字符串做爲 value 會更安全:
enum types { Folder = "FOLDER" } fetch(`/api?type=${monaco.languages.types.Folder}`);
最典型的是 chart 圖,常常是這樣的二維數組數據類型:
[[1, 5.5], [2, 3.7], [3, 2.0], [4, 5.9], [5, 3.9]]
通常咱們會這麼描述其數據結構:
const data: string[][] = [[1, 5.5], [2, 3.7], [3, 2.0], [4, 5.9], [5, 3.9]];
在 TS 2.7
版本中,咱們能夠更精確的描述每一項的類型與數組總長度:
interface ChartData extends Array<number> { 0: number; 1: number; length: 2; }
自動類型推導有兩種,分別是 typeof
:
function foo(x: string | number) { if (typeof x === "string") { return x; // string } return x; // number }
和 instanceof
:
function f1(x: B | C | D) { if (x instanceof B) { x; // B } else if (x instanceof C) { x; // C } else { x; // D } }
在 TS 2.7
版本中,新增了 in
的推導:
interface A { a: number; } interface B { b: string; } function foo(x: A | B) { if ("a" in x) { return x.a; } return x.b; }
這個解決了 object
類型的自動推導問題,由於 object
既沒法用 keyof
也沒法用 instanceof
斷定類型,所以找到對象的特徵吧,不再要用 as
了:
// Bad function foo(x: A | B) { // I know it's A, but i can't describe it. (x as A).keyofA; } // Good function foo(x: A | B) { // I know it's A, because it has property `keyofA` if ("keyofA" in x) { x.keyofA; } }
Typescript 2.0-2.9
文檔總體讀下來,能夠看出仍是有較強連貫性的。但咱們可能並不習慣一步步學習新語法,由於新語法須要時間消化、同時要鏈接到以往語法的上下文才能更好理解,因此本文從功能角度,而非版本角度梳理了 TS 的新特性,比較符合學習習慣。
另外一個感悟是,咱們也許要用追月刊漫畫的思惟去學習新語言,特別是 TS 這種正在發展中,而且迭代速度很快的語言。
討論地址是: 精讀《Typescript2.0 - 2.9》 · Issue #85 · dt-fe/weekly
若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。