关于javascript:你不知道的-import-type

13次阅读

共计 6081 个字符,预计需要花费 16 分钟才能阅读完成。

背景

TypeScript 3.8 带来了一个新个性:仅仅导入 / 导出申明

在上一篇文章中,咱们应用了这个个性,解决了: 引入类型文件报错 的问题。

其实这个个性并不简单,然而咱们须要理解其背地的机制原理,并理解 Babel 和 TypeScript 是如何一起工作的。

本文次要内容:

  • 什么是「仅仅导入 / 导出申明」
  • Babel 和 TypeScript 是如何一起工作的

注释

首先,先介绍一下这个个性。

什么是「仅仅导入 / 导出申明」

为了能让咱们导入类型,TypeScript 重用了 JavaScript 导入语法。

例如在上面的这个例子中,咱们确保 JavaScript 的值 doThing 以及 TypeScript 类型 Options 一起被导入:

// ./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);
}

这很不便的,因为在大多数的状况下,咱们不用放心导入了什么 —— 仅仅是咱们想导入的内容。

遗憾的是,这仅是因为一个被称之为「导入省略」的性能而起作用。

当 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";

报错:

一些了解:

  • 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

正文完
 0