阐述 JavaScript TypeScript 的类型推断 (Type Inference)、控制流分析 (Control Flow Analysis) 和声明文件 (.d.ts) 的生成与使用。

各位观众老爷们,大家好!我是你们的老朋友,今天要跟大家唠唠嗑,聊聊 TypeScript 里的几位重要人物:类型推断、控制流分析,还有声明文件这哥仨。 咱们的目标是,让大家听完之后,不仅知道它们是啥,还能在实际工作中玩转它们。准备好了吗?咱们这就开讲!

第一幕:类型推断——“它猜你心里想什么”

类型推断,英文名叫 Type Inference,听起来是不是很唬人?其实简单来说,就是 TypeScript 编译器“猜”你的变量、表达式的类型。它不用你显式地告诉它,自己就能分析出来。这就像是你跟你的老朋友,一个眼神,对方就知道你要干啥。

1.1 基础类型推断

最简单的例子:

let message = "Hello, TypeScript!"; // TypeScript 推断 message 的类型为 string
// message = 123; // 报错:不能将类型“number”分配给类型“string”

你看,咱们没写 let message: string = "Hello, TypeScript!";,TypeScript 也知道 message 是字符串类型。这就是类型推断的威力!

再来一个数字的例子:

let count = 10; // TypeScript 推断 count 的类型为 number
// count = "abc"; // 报错:不能将类型“string”分配给类型“number”

1.2 函数返回值类型推断

函数返回值也可以被推断:

function add(a: number, b: number) {
  return a + b; // TypeScript 推断函数的返回类型为 number
}

let result = add(5, 3); // result 的类型为 number

function greet(name: string) {
  console.log(`Hello, ${name}!`);
  // 没有显式返回值,TypeScript 推断返回类型为 void
}

let greeting = greet("Alice"); // greeting 的类型为 void

注意,如果函数没有 return 语句,或者 return 语句没有返回值,TypeScript 会推断返回类型为 void

1.3 最佳通用类型推断

当从几个表达式中推断类型时,会使用这些表达式的“最佳通用类型”。 比如:

let arr = [1, 2, null]; // TypeScript 推断 arr 的类型为 (number | null)[]

TypeScript 会找到一个所有类型都兼容的类型。 在这个例子中,numbernull 的最佳通用类型是 number | null

1.4 上下文类型推断

有时候,类型推断会依赖于上下文。

window.addEventListener("click", function(event) {
  // TypeScript 知道 event 是 MouseEvent 类型
  console.log(event.clientX, event.clientY);
});

在这个例子中,addEventListener 的第一个参数 "click" 告诉 TypeScript,回调函数的 event 参数应该是 MouseEvent 类型。

1.5 技巧:利用类型推断写出更简洁的代码

类型推断可以让我们少写很多类型注解,让代码更简洁。但要注意,过度依赖类型推断可能会降低代码的可读性。所以,要在简洁和可读性之间找到平衡。

第二幕:控制流分析——“程序执行到哪儿,它都知道”

控制流分析,英文名叫 Control Flow Analysis,是 TypeScript 编译器分析程序执行路径的一种技术。它能根据 ifelseswitch、循环等语句,推断出变量在不同代码分支中的类型。

2.1 可辨识联合类型

可辨识联合类型(Discriminated Unions)是控制流分析的一个重要应用场景。 想象一下,你有一个形状的类型,它可以是圆形、矩形或三角形。

interface Circle {
  kind: "circle";
  radius: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius * shape.radius; // TypeScript 知道 shape 是 Circle 类型
    case "rectangle":
      return shape.width * shape.height; // TypeScript 知道 shape 是 Rectangle 类型
    case "triangle":
      return 0.5 * shape.base * shape.height; // TypeScript 知道 shape 是 Triangle 类型
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

在这个例子中,shape.kind 就是一个辨识属性。 TypeScript 根据 shape.kind 的值,就能确定 shape 的具体类型。在每个 case 语句中,TypeScript 都能正确地推断出 shape 的类型,并进行类型检查。

2.2 类型收窄

类型收窄 (Type Narrowing) 是控制流分析的另一个关键概念。 它是指在代码块中,根据某些条件,将变量的类型从一个联合类型收窄到一个更具体的类型。

function printLength(str: string | null) {
  if (str) {
    // TypeScript 知道 str 在这里是 string 类型
    console.log(str.length);
  } else {
    // TypeScript 知道 str 在这里是 null 类型
    console.log("String is null");
  }
}

在这个例子中,if (str) 这个条件语句就把 str 的类型从 string | null 收窄到了 string

2.3 instanceof 操作符

instanceof 操作符也可以用于类型收窄。

class Animal {
  move() {
    console.log("Moving...");
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function doSomething(animal: Animal) {
  if (animal instanceof Dog) {
    // TypeScript 知道 animal 在这里是 Dog 类型
    animal.bark();
  } else {
    animal.move();
  }
}

2.4 自定义类型保护

有时候,TypeScript 无法自动推断类型,我们需要自定义类型保护函数来帮助它。

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function printDouble(x: number | string) {
  if (isNumber(x)) {
    // TypeScript 知道 x 在这里是 number 类型
    console.log(x * 2);
  } else {
    // TypeScript 知道 x 在这里是 string 类型
    console.log(x.toUpperCase());
  }
}

isNumber(x): x is number 就是一个类型保护函数。 它的返回值类型 x is number 告诉 TypeScript,如果 isNumber(x) 返回 true,那么 x 就是 number 类型。

2.5 技巧:善用控制流分析,写出更健壮的代码

控制流分析可以帮助我们发现代码中的潜在问题,并提高代码的健壮性。 比如,它可以帮助我们避免访问 nullundefined 类型的属性,或者在不正确的类型上调用方法。

第三幕:声明文件 (.d.ts) ——“给 JavaScript 代码配个翻译”

声明文件(Declaration Files),扩展名为 .d.ts,是用来描述 JavaScript 代码类型信息的文件。 它们不包含任何可执行代码,只包含类型声明。

3.1 为什么需要声明文件?

因为 JavaScript 是一种动态类型语言,没有类型信息。 当 TypeScript 代码需要使用 JavaScript 代码时,就需要声明文件来告诉 TypeScript 这些 JavaScript 代码的类型信息。 就像是给 JavaScript 代码配了个翻译,让 TypeScript 能够理解它。

3.2 如何生成声明文件?

  1. 手动编写: 对于简单的 JavaScript 库,可以手动编写声明文件。

  2. 使用 tsc 编译器: 可以通过 tsc 编译器的 --declaration 选项自动生成声明文件。

    tsc --declaration index.ts

    这条命令会生成一个 index.d.ts 文件,其中包含了 index.ts 中导出的类型声明。

  3. 使用第三方工具: 有一些第三方工具可以帮助生成声明文件,比如 dts-gen

3.3 声明文件的内容

声明文件主要包含以下内容:

  • 类型声明: 使用 typeinterface 关键字声明类型。
  • 变量声明: 使用 declare letdeclare constdeclare var 关键字声明变量。
  • 函数声明: 使用 declare function 关键字声明函数。
  • 类声明: 使用 declare class 关键字声明类。
  • 模块声明: 使用 declare module 关键字声明模块。

3.4 声明文件的例子

假设我们有一个 JavaScript 文件 utils.js

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

const PI = 3.14159;

module.exports = {
  add,
  PI
};

我们可以为它编写一个声明文件 utils.d.ts

// utils.d.ts
declare module "utils" {
  function add(a: number, b: number): number;
  const PI: number;
}

或者,如果使用 ES 模块:

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

export const PI = 3.14159;

对应的声明文件 utils.d.ts

// utils.d.ts
export function add(a: number, b: number): number;
export const PI: number;

3.5 如何使用声明文件?

  1. 与 JavaScript 文件放在同一目录下: TypeScript 编译器会自动查找与 JavaScript 文件同名的声明文件。

  2. 使用 /// <reference types="..."/> 指令: 可以在 TypeScript 文件中使用 /// <reference types="..."/> 指令来引入声明文件。

    /// <reference types="./utils" />
    import { add, PI } from "./utils";
    
    console.log(add(1, 2));
    console.log(PI);
  3. tsconfig.json 文件中配置 typeRootstypes 选项: 可以指定 TypeScript 编译器查找声明文件的目录。

    {
      "compilerOptions": {
        "typeRoots": ["./typings"] // 指定声明文件所在的目录
      }
    }

3.6 DefinitelyTyped

DefinitelyTyped 是一个大型的 TypeScript 声明文件仓库,包含了许多流行的 JavaScript 库的声明文件。 我们可以使用 npm install @types/<package-name> 命令来安装这些声明文件。

例如,要安装 Lodash 的声明文件:

npm install @types/lodash

3.7 技巧:编写高质量的声明文件,让你的 JavaScript 代码更易于使用

编写高质量的声明文件可以提高 JavaScript 代码的可重用性和可维护性。 在编写声明文件时,要尽可能地提供准确、完整的类型信息。

总结

今天咱们聊了 TypeScript 的三个重要特性:类型推断、控制流分析和声明文件。

特性 作用 优点 缺点
类型推断 自动推断变量、表达式的类型 减少代码中的类型注解,提高代码简洁性 过度依赖类型推断可能会降低代码的可读性,需要权衡
控制流分析 分析程序执行路径,推断变量在不同代码分支中的类型 提高代码的健壮性,避免潜在的类型错误 对于复杂的控制流,TypeScript 可能无法正确推断类型,需要手动进行类型收窄
声明文件 (.d.ts) 描述 JavaScript 代码的类型信息,让 TypeScript 能够理解 JavaScript 代码 允许 TypeScript 代码使用 JavaScript 代码,提高代码的可重用性和可维护性 编写声明文件需要额外的工作量,需要保证声明文件的准确性和完整性

希望今天的讲座能帮助大家更好地理解和使用 TypeScript。 记住,实践是检验真理的唯一标准。 多写代码,多思考,才能真正掌握这些知识。

好了,今天的讲座就到这里。 谢谢大家! 咱们下回再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注