专栏首页前端皮小蛋你不知道的 「 import type 」

你不知道的 「 import type 」

背景

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

报错:

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

本文分享自微信公众号 - 前端皮小蛋(gh_e69260c16440),作者:南山皮小蛋

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-04-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 我知道你不知道GB2312

    这篇文章将是大猫《如何搞定头疼的编码》一文的一部分,当时本来想做一个完整的有关“R与编码”的笔记,没想到后来洋洋洒洒写了六七千字,估计一时半会也完成不了,所以先...

    用户7652506
  • 你不知道的HashMap

    面试中经常会问到常用数据结构,比如HashMap。 相信你平时几乎每天都会用到HashMap,但是你知道它是的实现原理是怎样的吗? 这里先提几个问题:HashM...

    PhoenixZheng
  • 你不知道的console.log

    对于前端开发者,使用console.log() 次数绝对很多,但是大部分人认识的 console 对象还不是很全面,其实深入了解这些后,你会发现给开发过程带来很...

    游魂
  • 你不知道的3GPP

    智能手机已经成为了我们日常生活中必不可少的一部分,根据消费者行为分析公司comScore的分析显示,普通消费者每天花费近3个小时通过智能手机使用流媒体、共享、通...

    SDNLAB
  • 你不知道的 Javascript

    作用域 词法作用域:编译阶段确定(欺骗词法作用域 eval with) JavaScript 1 2 3 4 5 6function foo(str){ "u...

    用户1667431
  • 你不知道的 requestIdleCallback

    本文副标题是 Request Schedule 源码解析一。在本章中会介绍 requestIdleCallback 的用法以及其缺陷, 接着对 React 团队...

    牧云云
  • 你不知道的JSON.stringify

    replacer 可以作为一个函数传入,且接受 key 和 value 作为入参,如下:

    前端黑板报
  • 你不知道的 GraphQL

    很久之前其实就关注过这个技术,记得当时还是React刚刚崭露头角的时期吧。总之那时候,GraphQL感觉还只是概念完备阶段,除了FB自己内部大量使用外,好像社区...

    一只图雀
  • 你不知道的 MutationObserver

    在某些场景下,我们希望能监视 DOM 树的变动,然后做一些相关的操作。比如监听元素被插入 DOM 或从 DOM 树中移除,然后添加相应的动画效果。或者在富文本编...

    阿宝哥
  • 你不知道的npm

    作为 node 自带的包管理器工具,在 nodejs 社区和 web 前端工程化领域发展日益庞大的背景下,npm已经成为每位前端开发同学必备的工具。

    前端森林
  • 你不知道的Synchronized

    众所周知,synchronized关键字是Java语言中一种重要的同步机制,它既可以修饰方法,也可以修饰代码块。synchronized使用了对象的内部锁,使得...

    张申傲
  • 你不知道的 Blob

    如果你允许用户从你的网站上下载某些文件,那你可能会遇到 Blob 类型。为了实现上述的功能,你可以很容易从网上找到相关的示例,并根据实际需求进行适当的调整。对于...

    阿宝哥
  • 你不知道的 WeakMap

    相信很多读者对 ES6 引入的 Map 已经不陌生了,其中的一部分读者可能也听说过 WeakMap。既生 Map 何生 WeakMap?带着这个问题,本文将围绕...

    阿宝哥
  • 你不知道的PBMC

    指外PBMC(peripheral blood mononuclear cell,外周血单核细胞),指外周血中具有单个核的细胞,包含淋巴细胞、单核细胞等。外周血...

    生信交流平台
  • 你不知道的 CSS

    本文的每一条,都是我曾经发过的掘金沸点,其中有很多条超过了百赞(窃喜)。鉴于时不时有童鞋翻我以前的沸点,因此,本文收集了个人目前发过的所有CSS知识点动图,以便...

    石燕平
  • 你知道和你不知道的冒泡排序

    我看过的很多的文章都把冒泡排序描述成我们喝的汽水,底部不停的有二氧化碳的气泡往上冒,还有描述成鱼吐泡泡,都特别的形象。

    SH的全栈笔记
  • 你知道和你不知道的选择排序

    然后我们再通过我制作的gif,配上数据再了解一下过程。假设我们的待排序数组还是[5, 1, 3, 7, 6, 2, 4]。

    SH的全栈笔记
  • 我不知道你知不知道我知道的伪元素小技巧

    伪元素能做什么?我们要他有何用?它能为我们解决什么问题?和其他的方法相比她有什么有点?我们为什么要使用它?

    sunseekers
  • JVM 你知道不

    关于 Java 虚拟机的学习,无论从业务开发上来说,还是从解决问题的角度上来看。我认为都是一个 Java 程序员必会的一个知识体系,为什么这么说呢?还是那句耳朵...

    星尘的一个朋友

扫码关注云+社区

领取腾讯云代金券