深度思考:前端猿进阶100问(TS篇)

471 阅读14分钟

为何本地开发时我们导入的内容都是由js/ts模块导出的,而第三方包几乎全部由dts导出,是何道理?

在JavaScript/TypeScript的生态系统中,模块导出和.dts文件扮演着不同但互补的角色。

理解这两者的区别和用途有助于明白为什么本地开发中通常使用JS/TS模块导出、而第三方库往往伴随着.d.ts文件。

JS/TS模块导出

  • JS/TS模块导出是指在JavaScript或TypeScript文件中使用export关键字导出变量、函数、类等,以便其他文件通过import关键字导入和使用。
  • 这是模块化编程的基础,允许开发者将代码拆分成可重用的单元。
  • 在TypeScript项目中,TS代码在编译时会被转换成JS代码。
  • 这个过程中,TypeScript的类型信息会被移除,因为JavaScript本身不支持静态类型检查。

dts文件导出

  • .d.ts文件,或称为声明文件,是TypeScript的一个特性,用于提供JavaScript代码的类型信息。
  • 这些文件不包含实际的代码逻辑,只包含类型声明
  • 使得TypeScript编译器能够在不执行代码的情况下进行类型检查。
  • 很多老的第三方JavaScript库本身不是用TypeScript编写的
  • 但为了在TypeScript项目中使用这些库而不丢失类型安全性,库的作者或社区会提供相应的.d.ts声明文件。
  • 这样,即使库的源代码是用纯JavaScript编写的,开发者也能享受到TypeScript的类型检查和智能提示等特性。

为什么第三方包使用dts

  • 兼容性:许多流行的JavaScript库和框架(如React、Lodash等)在TypeScript变得流行之前就已经存在了。为了使这些已经广泛使用的库能够在TypeScript项目中无缝使用,社区会为它们创建.d.ts声明文件。
  • 性能:.d.ts文件只包含类型信息,不包含实际的逻辑代码。这意味着它们不会增加最终JavaScript代码的体积,也不会影响运行时性能。
  • 开发体验:即使是用JavaScript编写的库,通过提供.d.ts文件,也能让TypeScript用户享受到类型检查、自动补全和接口文档等开发时的好处。

总结

  • 我们在本地开发中直接使用JS/TS模块导出是因为我们正在编写和维护这些代码,需要它们被编译和执行。
  • 而第三方包使用.d.ts文件是为了提供给这些通常是用JavaScript编写的库的TypeScript类型信息,以提高在TypeScript项目中使用这些库时的 开发体验类型安全 性。

很多第三方库中的js文件都有同名的dts文件,这些文件是猿类动物手写的,还是用什么工具自动生成的呢?

对于第三方JavaScript库的.d.ts文件(类型声明文件),它们既可以是手写的,也可以是通过工具自动生成的。这取决于库的作者或维护者以及社区的贡献者如何选择处理类型声明。下面是一些常见的情况

手写的dts文件

  • 对于一些复杂的库,或者那些对类型精度有高要求的库,维护者和贡献者可能会选择手动编写.d.ts文件。
  • 手写可以提供最准确和最详细的类型声明,尤其是在库的行为难以用自动生成工具准确描述的情况下。
  • 手写类型声明文件也允许作者提供更丰富的注释和文档,这对于使用者来说是非常有价值的。

自动生成的dts文件

  • 自动生成:随着工具链的发展,现在有多种工具可以从JavaScript代码中自动生成.d.ts文件。
  • 这些工具通常依赖于JSDoc注释类型推断来生成类型声明。
  • TypeScript:TypeScript编译器(tsc)本身就可以生成.d.ts文件。
  • 如果一个库是用TypeScript写的,那么在编译过程中生成声明文件是非常直接的。
  • 即使是纯JavaScript项目,只要适当地使用了JSDoc注释,也可以配置TypeScript编译器来生成类型声明文件。
  • dts-gen:dts-gen是一个由Microsoft维护的工具,可以帮助生成模块的初始声明文件。
  • 它尝试自动推断类型信息,但生成的声明可能需要手动调整和完善。
  • 其他工具:还有其他一些工具和库,如jsdoc-to-tstsd-jsdoc,它们可以从JSDoc注释生成TypeScript声明文件。
  • 这些工具的效果各不相同,通常需要一定程度的手动调整。

社区贡献的dts文件

  • 对于那些原生不支持TypeScript的流行JavaScript库,.d.ts文件通常由社区贡献者手动编写并维护
  • DefinitelyTyped是一个著名的项目,它是一个巨大的类型声明库,为成千上万的JavaScript库提供了高质量的TypeScript声明文件。
  • 这些文件经过社区的严格审查,以确保类型的准确性。

总的来说,.d.ts文件可以通过多种方式产生,不同的项目和库可能会根据其特定的需求和资源选择最适合它们的方法。


能就手写和自动生成dts文件,分别给一个简单明了的实例吗?

假设我们有一个小型的JavaScript库,它提供了一个函数add,用于计算两个数字的和。

// math.js
export function add(a, b) {
    return a + b;
}

手动编写dts文件

为了在TypeScript项目中使用上述add函数而不丢失类型信息,我们可以手动创建一个声明文件math.d.ts。

// math.d.ts
export function add(a: number, b: number): number;

这个声明文件告诉TypeScript编译器add函数接受两个number类型的参数,并返回一个number类型的结果。

自动生成dts文件

要自动生成.d.ts文件,我们首先需要确保我们的JavaScript代码包含了足够的JSDoc注释,以便工具能够从中推断出类型信息。

// math.js
/**
 * Adds two numbers.
 * @param {number} a The first number.
 * @param {number} b The second number.
 * @returns {number} The sum of the two numbers.
 */
export function add(a, b) {
    return a + b;
}

或者直接上TS当然就更OK了,两者生成的dts效果是等价的

// math.ts
export function add(a:number, b:number):number {
    return a + b;
}

接下来,我们可以使用TypeScript编译器(tsc)来生成.d.ts文件。首先,你需要一个简单的tsconfig.json文件,指定编译器选项。

{
    "compilerOptions": {
        "allowJs": true,
        "declaration": true,
        "outDir": "./types"
    },
    "include": [
        "./math.js"
    ]
}

然后,运行tsc命令,它会根据math.js文件中的JSDoc注释生成相应的math.d.ts文件。

生成的math.d.ts可能看起来像这样:

// types/math.d.ts
export function add(a: number, b: number): number;

或者

// types/math.d.ts
// 这里declare有没有都不影响编译器的实际编译效果
// declare的作用仅仅在于 从语义上提示你 这个类型的具体实现已经存在了 此处仅仅只是一个类型声明
export declare function add(a: number, b: number): number;

注意,自动生成的声明文件的准确性高度依赖于原始JavaScript代码中JSDoc注释的质量。在某些情况下,自动生成的声明可能需要手动调整以修正类型错误或遗漏。

通过这个简单的例子,你可以看到手动编写和自动生成.d.ts文件各自的过程和结果。选择哪种方法取决于项目的具体需求、资源以及对类型准确性的要求。


dts中的declare语法到底有何卵用,举例说明?

在TypeScript中,declare关键字用于声明变量、类、函数等的类型,而不定义它们的实现。这在.d.ts文件中尤其常见,因为.d.ts文件的目的是提供类型信息,而不是执行逻辑。declare关键字的使用场景包括但不限于:

1. 声明全局变量:当你的JavaScript代码依赖于在其他地方定义的全局变量时,你可以使用declare来告诉TypeScript这个变量的类型。

2. 声明模块:当你使用非TypeScript编写的模块时(比如,从CDN加载的库),你可以使用declare module来提供这个模块的类型信息。

3. 声明类:当你需要声明一个JavaScript类的类型信息时,可以使用declare class。

4. 声明类型:可以使用declare type或declare interface来声明自定义类型或接口。

声明全局变量

假设你的网页中有一个全局变量MY_APP_CONFIG,它是在其他脚本中定义的,你可以这样声明它的类型:

// globals.d.ts
declare var MY_APP_CONFIG: {
    apiKeystring;
    apiUrlstring;
};

这样,你就可以在TypeScript文件中安全地访问MY_APP_CONFIG的属性,且享有类型检查。

声明模块

如果你正在使用一个JavaScript库,比如some-js-library,但这个库没有提供TypeScript类型声明,你可以创建一个声明文件来补充:

// some-js-library.d.ts
declare module 'some-js-library' {
    export function doSomething(): void;
}

这允许你在TypeScript文件中import并使用some-js-library,同时享有类型安全。

声明类

对于在JavaScript中定义的类,你可以这样声明它的类型:

// MyClass.d.ts
declare class MyClass {
    constructor(someParam: string);
    someMethod(): number;
}

这对于声明第三方JavaScript库中的类非常有用。

声明类型和接口

当需要在多个地方使用同一类型时,可以使用declare来定义类型或接口:

// types.d.ts
declare interface User {
    namestring;
    agenumber;
}

这样,你就可以在任何TypeScript文件中引用User接口了。

总结

declare关键字在.d.ts文件中的使用非常广泛,它允许开发者为已存在的JavaScript代码提供类型信息,从而使TypeScript项目能够利用这些信息进行类型检查和代码提示,而不需要改变原有的JavaScript代码。


declare前加不加export有何区别

在.d.ts文件中,declare和export关键字用于声明和导出类型。它们的组合或单独使用会影响类型的可见性和使用方式。

使用declare不加export

当你在.d.ts文件中使用declare关键字而不加export时,你正在声明一个全局类型。这意味着该类型在整个项目中都是可见的,无需通过import语句显式导入。

// example.d.ts
declare namespace MyNamespace {
    function doSomething(): void;
}

在这个例子中,MyNamespace和doSomething函数在包含此声明文件的任何地方都可以直接使用,无需导入。

使用declare加export

当你在.d.ts文件中使用declare关键字并加上export时,你正在声明一个模块作用域内的类型。这意味着该类型只能在通过import语句导入它的文件中使用。

// example.d.ts
declare module "myModule" {
    export function doSomething(): void;
}

或者,直接在顶层使用export声明类型:

// example.d.ts
export declare function doSomething(): void;

在这两个例子中,要使用doSomething函数,你需要通过import语句显式导入它,如下所示:

import { doSomething } from "myModule";
// 或者
import { doSomething } from "./example";

总结

  • 不加export:声明的类型为全局类型,项目中的任何地方都可以直接使用,无需导入。这适用于声明全局变量、函数或在多个文件中共享的类型。
  • 加export:声明的类型为模块作用域内的类型,只能在导入该模块的文件中使用。这适用于模块化开发,当你希望控制类型的可见性和使用范围时。
  • 在.d.ts文件或TypeScript的类型声明上下文中,declare关键字的主要作用是从语义上指示某个实体(变量、函数、类等)的声明已经存在于某处,而当前仅提供其类型信息,不包含实现细节。
  • 这对于告知TypeScript编译器如何处理这些声明非常重要,尤其是在处理全局声明或模块声明时。

import或import type VS declare

我完全不用declare,而使用import或import type不是也可以吗?所以究竟有何区别?

是的,你可以通过import或import type来引入类型信息,而不必总是使用declare。这两种方法各有适用场景,理解它们的差异有助于你更有效地使用TypeScript。

使用import或import type

  • 当你在使用模块化的JavaScript或TypeScript项目时,import和import type是引入其他文件或模块中定义的变量、函数、类、接口等的标准方式。
  • import type是TypeScript 3.8引入的特性,允许你仅导入类型而不是运行时的对象。
  • import:用于导入另一个模块导出的变量、函数、类等。这些导入的对象在编译后的JavaScript代码中会保留引用,影响运行时行为。
  • import type:仅用于导入类型信息,如接口、类型别名等。这些导入的类型在编译后的代码中会被完全移除,不影响运行时。

使用declare

declare关键字用于声明环境中已存在的变量、模块、类等的类型信息,而不引入实际的实现。这在以下几种情况下特别有用:

  • 声明全局变量的类型:当你的代码运行在一个特定的环境中,这个环境提供了一些全局变量,你可以使用declare var来告诉TypeScript这些全局变量的类型。
  • 声明模块类型:对于没有提供.d.ts文件的第三方JavaScript库,你可以使用declare module来手动声明模块的类型信息,使其可以在TypeScript中使用。
  • 扩展现有类型:当你想要扩展一个库中的类型,但不想或不能修改原始类型定义时,可以使用declare来增加新的类型声明。

选择使用import/import type还是declare

  • 如果你在处理模块化的代码,且目标模块或库提供了TypeScript类型定义(无论是内置的还是通过DefinitelyTyped等社区项目提供的),那么使用import或import type是更合适的选择。
  • 如果你需要在TypeScript中使用非模块化的JavaScript库,或者需要声明全局变量、模块等的类型信息,而这些信息在你的项目中并不存在实际的导出,那么使用declare是必要的。

总结

总的来说,import和import type适用于模块化代码和类型的导入,而declare适用于声明环境中已存在的全局变量、模块等的类型信息的追加补充声明。


dts中的三斜线语法到底有何卵用,举例说明

在.d.ts文件或TypeScript文件中,三斜线指令(Triple-Slash Directives)是一种特殊的语法,用于包含或引用其他文件的声明。这种语法主要在以下几个方面发挥作用:

1. 引用类型声明文件:当你需要在一个文件中直接引用另一个声明文件时,可以使用三斜线引用指令。
2. 编译器指令:它可以用来给TypeScript编译器传递特定的指令,比如指定编译选项或引用其他文件。

语法

三斜线指令看起来像这样:

/// <reference path="..." />

它必须放在文件的最顶部,除了文件的首行可以是#!(用于指定脚本解释器)。

引用类型声明文件

假设你有两个声明文件,node.d.ts和express.d.ts,而express.d.ts依赖于node.d.ts中的类型。你可以在express.d.ts的顶部使用三斜线指令来确保node.d.ts被正确引用:

/// <reference path="node.d.ts" />

这告诉TypeScript编译器在处理express.d.ts之前先处理node.d.ts文件。

编译器指令

三斜线指令也可以用来指定编译器选项。例如,lib指令允许你指定编译过程中包含的库文件:

/// <reference lib="es2015" />

这个指令告诉TypeScript编译器在编译当前文件时,应该包含ES2015的类型定义。

备注

  • 随着TypeScript的发展,特别是模块系统和tsconfig.json的引入,三斜线指令的使用变得越来越少。
  • 大多数情况下,推荐使用import语句或在tsconfig.json中配置来管理类型依赖和编译器选项,因为这些方法更加灵活和强大。
  • 然而,在一些特定的场景下,特别是在处理全局声明和兼容旧代码时,三斜线指令仍然有其用武之地。