你不知道的 「 import type 」

image.png

背景

TypeScript 3.8 帶來了一個新特性:僅僅導入 / 導出聲明html

上一篇文章中, 咱們使用了這個特性,解決了: 引入類型文件報錯的問題。react

其實這個特性並不複雜,可是咱們須要瞭解其背後的機制原理,並瞭解 Babel 和 TypeScript 是如何一塊兒工做的。git

本文主要內容:github

  • 什麼是「 僅僅導入 / 導出聲明 」
  • Babel和TypeScript是如何一塊兒工做的

正文

首先, 先介紹一下這個特性。typescript

什麼是「 僅僅導入 / 導出聲明 」

爲了能讓咱們導入類型,TypeScript 重用了 JavaScript 導入語法。segmentfault

例如在下面的這個例子中,咱們確保 JavaScript 的值 doThing 以及 TypeScript 類型 Options 一同被導入:api

// ./foo.ts
interface Options {
  // ...
}

export function doThing(options: Options) {
  // ...
}

// ./bar.ts
import { doThing, Options } from './foo.js';

function doThingBetter(options: Options) {
  // do something twice as good
  doThing(options);
  doThing(options);
}

這很方便的,由於在大多數的狀況下,咱們沒必要擔憂導入了什麼 —— 僅僅是咱們想導入的內容。babel

遺憾的是,這僅是由於一個被稱之爲「導入省略」的功能而起做用。ide

當 TypeScript 輸出一個 JavaScript 文件時,TypeScript 會識別出 Options 僅僅是看成了一個類型來使用,它將會刪除 Options。工具

// ./foo.js
export function doThing(options: Options) {
  // ...
}

// ./bar.js
import { doThing } from './foo.js';

function doThingBetter(options: Options) {
  // do something twice as good
  doThing(options);
  doThing(options);
}

在一般狀況下,這種行爲都是比較好的。可是它會致使一些其餘問題。

首先,在一些場景下,TypeScript 會混淆導出的到底是一個類型仍是一個值。

好比在下面的例子中, MyThing 到底是一個值仍是一個類型?

import { MyThing } from './some-module.js';

export { MyThing };

若是單從這個文件來看,咱們無從得知答案。

若是 Mything 僅僅是一個類型,Babel 和 TypeScript 使用的 transpileModule API 編譯出的代碼將沒法正確工做,而且 TypeScript 的 isolatedModules 編譯選項將會提示咱們,這種寫法將會拋出錯誤。

問題的關鍵在於,沒有一種方式能識別它僅僅是個類型,以及是否應該刪除它,所以「導入省略」並不夠好。

同時,這也存在另一個問題,TypeScript 導入省略將會去除只包含用於類型聲明的導入語句。

對於含有反作用的模塊,這形成了明顯的不一樣行爲。因而,使用者將會不得不添加一條額外的聲明語句,來確保有反作用。

// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from './module-with-side-effects';

// This statement always sticks around.
import './module-with-side-effects';

一個咱們看到的具體例子是出如今 Angularjs(1.x)中, services 須要在全局在註冊(它是一個反作用),可是導入的 services 僅僅用於類型聲明中。

// ./service.ts
export class Service {
  // ...
}
register('globalServiceId', Service);

// ./consumer.ts
import { Service } from './service.js';

inject('globalServiceId', function(service: Service) {
  // do stuff with Service
});

結果 ./service.js 中的代碼不會被執行,致使在運行時會被中斷。

在 TypeScript 3.8 版本中,咱們添加了一個僅僅導入/導出 聲明語法來做爲解決方式。

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import type 僅僅導入被用於類型註解或聲明的聲明語句,它老是會被徹底刪除,所以在運行時將不會留下任何代碼。

與此類似,export type 僅僅提供一個用於類型的導出,在 TypeScript 輸出文件中,它也將會被刪除。

值得注意的是,類在運行時具備值,在設計時具備類型。它的使用與上下文有關。

當使用 import type 導入一個類時,你不能作相似於從它繼承的操做。

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

若是在以前你使用過 Flow,它們的語法是類似的。

一個不一樣的地方是咱們添加了一個新的限制條件,來避免可能混淆的代碼。

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

與 import type 相關聯,咱們提供來一個新的編譯選項:importsNotUsedAsValues,經過它能夠來控制沒被使用的導入語句將會被如何處理,它的名字是暫定的,可是它提供來三個不一樣的選項。

  • remove,這是如今的行爲 —— 丟棄這些導入語句。這仍然是默認行爲,沒有破壞性的更改
  • preserve,它將會保留全部的語句,即便是歷來沒有被使用。它能夠保留反作用。
  • error,它將會保留全部的導入(與 preserve 選項相同)語句,可是當一個值的導入僅僅用於類型時將會拋出錯誤。若是你想確保沒有意外導入任何值,這會是有用的,可是對於反作用,你仍然須要添加額外的導入語法。

對於該特性的更多信息,參考該 PR

Babel 和 TypeScript 是如何一塊兒工做的

TypeScript 作了兩件事

  1. 將靜態類型檢查添加到 JavaScript 代碼中。
  2. 將 TS + JS 代碼轉換爲各類JS版本。

Babel 也作第二件事。

Babel的方法(特別是transform-typescript插件時)是: 先刪除類型,而後進行轉換

這樣,就便可以使用 Babel 的全部優勢,同時仍然可以提供 ts 文件。

看個例子:

babel 編譯前:

// example.ts
import { Color } from "./types";
const changeColor = (color: Color) => {
  window.color = color;
};

babel 編譯後:

// example.js
const changeColor = (color) => {
  window.color = color;
};

在這裏,babel 不能告訴 example.ts 那個 Color 其實是一個類型。

所以,babel 也被迫錯誤地將此聲明保留了轉換後的代碼中。

爲何會這樣?

Babel在轉譯過程當中一次明確地處理一個文件。

大概是由於 babel 團隊並不想像 TypeScript 那樣, 在相同的類型解析過程當中進行構建,只是爲了刪除這些類型吧。

isolatedModules

isolatedModules 是什麼

isolatedModules是TypeScript編譯器選項,旨在充當保護措施。

tsc 作類型檢查時,當監測到 isolatedModules 是開啓的,就會報類型錯誤。

若是錯誤未解決,將影響獨立處理文件的編譯工具(babel)。

From TypeScript docs:

Perform additional checks to ensure that separate compilation (such as with transpileModule or @babel/plugin-transform-typescript) would be safe.

From Babel docs:

--isolatedModules This is the default Babel behavior, and it can't be turned off because Babel doesn't support cross-file analysis.

換句話說,每一個ts文件都必須可以獨立進行編譯。

isolatedModules 標誌可防止咱們引入模棱兩可的import。

下面看兩個具體的例子看幾個例子,瞭解 isolatedModules 標記的重要性。

1. 混合導入, 混合導出

在這裏,咱們採用在 types.ts 文件中定義的類型,而後從中從新導出它們。

打開 isolatedModules 時,此代碼不會 經過類型檢查。

// types.ts
export type Playlist = {
  id: string;
  name: string;
  trackIds: string[];
};

export type Track = {
  id: string;
  name: string;
  artist: string;
  duration: number;
};

// lib-ambiguous-re-export.ts
export { Playlist, Track } from "./types";
export { CreatePlaylistRequestParams, createPlaylist } from "./api";

Babel 轉換後:

// dist/types.js
--empty--

// dist/lib-ambiguous-re-export.js
export { Playlist, Track } from "./types";
export { CreatePlaylistRequestParams, createPlaylist } from "./api";

報錯:

image.png

一些理解:

  • Babel 從咱們的types模塊中刪除了全部內容,它僅包含類型。
  • Babel 沒有對咱們的 lib 模塊進行任何轉換。Playlist 而且 Track 應該由 Babel 移除。從Node 的角度來看,Node 作模塊解析時,會發現 types.js 中引入的文件是空的,報錯:文件不存在。
  • 如截圖所示,tsc 類型檢查過程當即將這些模糊的從新導出報告爲錯誤。

2. 顯式類型導入,顯式類型導出

此次,咱們明確地將中的類型從新導出lib-import-export.ts。

打開 isolatedModules時,此代碼將經過 tsc 類型檢查。

編譯前:

// types.ts
export type Playlist = {
  id: string;
  name: string;
  trackIds: string[];
};

// lib-import-export.ts
import {
  Playlist as PlaylistType,
  Track as TrackType,
} from "./types";

import {
  CreatePlaylistRequestParams as CreatePlaylistRequestParamsType,
  createPlaylist
} from "./api";

export type Playlist = PlaylistType;
export type Track = TrackType;
export type CreatePlaylistRequestParams = CreatePlaylistRequestParamsType;
export { createPlaylist };

編譯後:

// dist/types.js
--empty-- TODO or does babel remove it all together?

// lib-import-export.js
import { createPlaylist } from "./api";
export { createPlaylist };

此時:

  • Babel仍輸出一個空 types.js 文件。但這不要緊,由於咱們編譯的lib-import-export.js器沒再引用它。

TypeScript 3.8

如先前介紹, TypeScript 3.8 引入了新的語法 -- 「 僅僅導入 / 導出聲明 」。

該語法在使用時爲類型解析過程增長了肯定性。

如今,編譯器(不管是tsc,babel仍是其餘)都將可以查看單個文件,並取消導入或導出(若是它是TypeScript類型)。

import type ... from — 讓編譯器知道您要導入的內容絕對是一種類型。

export type ... from — 同樣, 僅用做導出。

// src/lib-type-re-export.ts
export type { Track, Playlist } from "./types";
export type { CreatePlaylistRequestParams } from "./api";
export { createPlaylist } from "./api";

// 會被編譯爲:

// dist/lib-type-re-export.js
export { createPlaylist } from "./api";

更多參考

  1. TS文檔的新部分:https://www.typescriptlang.org/docs/handbook/modules.html#importing-types
  2. 引入了類型導入的TS PR。PR說明中有不少很棒的信息:https : //github.com/microsoft/TypeScript/pull/35200
  3. TS 3.8公告:https : //devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#type-only-imports-exports
  4. Babel PR,加強了babel解析器和transform-typescript插件,以利用新語法。隨Babel 7.9一塊兒發佈:https : //github.com/babel/babel/pull/11171
相關文章
相關標籤/搜索