精讀《Typescript2.0 - 2.9》

1 引言

精讀原文是 typescript 2.0-2.9 的文檔:html

2.0-2.82.9 草案.前端

我發現,許多寫了一年以上 Typescript 開發者,對 Typescript 對理解和使用水平都停留在入門階段。形成這個現象的緣由是,Typescript 知識的積累須要 刻意練習,使用 Typescript 的時間與對它的瞭解程度幾乎沒有關係。vue

這篇文章精選了 TS 在 2.0-2.9 版本中最重要的功能,並配合實際案例解讀,幫助你快速跟上 TS 的更新節奏。webpack

對於 TS 內部優化的用戶無感部分並不會羅列出來,由於這些優化均可在平常使用過程當中感覺到。git

2 精讀

因爲 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 等因而函數返回值中的 nullundefined它們都是子類型,好比類型 number 自帶了 nullundefined 這兩個子類型,是由於任何有類型的值都有多是空(也就是執行期間可能沒有值)。

這裏涉及到很重要的概念,就是預約義了類型不表明類型必定如預期,就比如函數運行時可能由於 throw Error 而中斷。因此 ts 爲了處理這種狀況,null undefined 設定爲了全部類型的子類型,而從 2.0 開始,函數的返回值類型又多了一種子類型 never

TS 2.2 支持了 object 類型, 但許多時候咱們總把 objectany 類型弄混淆,好比下面的代碼:

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] };

能夠定義函數的 this 類型

也是 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]>,將對象每一個 keyvalue 類型覆蓋。

基於這些能力,咱們拓展出一系列上層頗有用的 interface

  • Readonly<T>。把對象 key 所有設置爲只讀,或者利用 2.8 的條件類型語法,實現遞歸設置只讀。
  • Partial<T>。把對象的 key 都設置爲可選。
  • Pick<T, K>。從對象類型 T 挑選一些屬性 K,好比對象擁有 10 個 key,只須要將 K 設置爲 "name" | "age" 就能夠生成僅支持這兩個 key 的新對象類型。
  • Extract<T, U>。是 Pick 的底層 API,直到 2.8 版本才內置進來,能夠認爲 Pick 是挑選對象的某些 key,Extract 是挑選 key 中的 key。
  • Record<K, U>。將對象某些屬性轉換成另外一個類型。比較常見用在回調場景,回調函數返回的類型會覆蓋對象每個 key 的類型,此時類型系統須要 Record 接口才能完成推導。
  • Exclude<T, U>。將 T 中的 U 類型排除,和 Extract 功能相反。
  • Omit<T, K>(未內置)。從對象 T 中排除 key 是 K 的屬性。能夠利用內置類型方便推導出來:type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
  • NonNullable<T>。排除 Tnullundefined 的可能性。
  • ReturnType<T>。獲取函數 T 返回值的類型,這個類型意義很大。
  • InstanceType<T>。獲取一個構造函數類型的實例類型。
以上類型都內置在 lib.d.ts 中,不須要定義就可直接使用,能夠認爲是 Typescript 的 utils 工具庫。

單獨拿 ReturnType 舉個例子,體現出其重要性:

Redux 的 Connect 第一個參數是 mapStateToProps,這些 Props 會自動與 React Props 聚合,咱們能夠利用 ReturnType<typeof currentMapStateToProps> 拿到當前 Connect 注入給 Props 的類型,就能夠打通 Connect 與 React 組件的類型系統了。

對 Generators 和 async/await 的類型定義

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 {
  //...
}

動態 Import

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

Enum 類型支持字符串

從 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;
  }
}

4 總結

Typescript 2.0-2.9 文檔總體讀下來,能夠看出仍是有較強連貫性的。但咱們可能並不習慣一步步學習新語法,由於新語法須要時間消化、同時要鏈接到以往語法的上下文才能更好理解,因此本文從功能角度,而非版本角度梳理了 TS 的新特性,比較符合學習習慣。

另外一個感悟是,咱們也許要用追月刊漫畫的思惟去學習新語言,特別是 TS 這種正在發展中,而且迭代速度很快的語言。

5 更多討論

討論地址是: 精讀《Typescript2.0 - 2.9》 · Issue #85 · dt-fe/weekly

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。

相關文章
相關標籤/搜索