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 也作第二件事。
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是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 標記的重要性。
在這裏,咱們採用在 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";
報錯:
一些理解:
空的
,報錯:文件不存在。此次,咱們明確地將中的類型從新導出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 };
此時:
如先前介紹, 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";