跳至主要內容

JavaScript 文件类型检查

樱桃茶大约 38 分钟

JavaScript 文件类型检查

JavaScript 文件类型检查是 TypeScript 的一个特性,它允许你在常规的 .js 文件中使用 TypeScript 类型系统进行错误检测。这个特性通过在 JavaScript 文件中添加 JSDoc 注释来实现。

  • 实用例子: 假设你有一个简单的 add 函数,在 JavaScript 中你可能这样写:

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

    在 TypeScript 中,你可以为函数参数和返回值指定类型,但是如果你想在 JavaScript 文件中受益于类型检查,就可以使用 JSDoc 注释:

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

    通过添加这些注释,TypeScript 编译器(或者支持 TypeScript 类型检查的编辑器,如 Visual Studio Code)可以在你对函数的调用中提供类型信息,并且在类型不正确时给出警告。

  • 当你在 VSCode 这样的编辑器中打开带有 JSDoc 注释的 .js 文件时,编辑器将会提供自动完成提示并且会标记潜在的类型错误。

  • 要启用此功能,你需要在 tsconfig.jsonjsconfig.json 中设置 "checkJs": true,这告诉 TypeScript 编译器检查 .js 文件中的错误。

  • 使用 JavaScript 文件类型检查可以逐步迁移到 TypeScript,因为你可以先在现有的 .js 文件中引入类型检查,然后逐渐地转换那些文件为 .ts 格式。

  • 除了基本类型检查,JSDoc 注释还支持更复杂的 TypeScript 功能,比如类型断言、类型别名、接口等。

  • 例如,你可以定义一个复杂对象的结构,并用它作为函数参数的类型:

    /**
     * @typedef {Object} User
     * @property {string} name
     * @property {number} age
     */
    
    /**
     * Creates a greeting message for a user.
     * @param {User} user An object representing a user.
     * @returns {string}
     */
    function greet(user) {
      return `Hello, ${user.name}!`;
    }
    

    在这个例子中,我们首先定义了一个 User 类型的结构,然后我们在 greet 函数的 JSDoc 注释中使用了这个类型。如果尝试传递一个缺少 nameage 属性的对象给 greet 函数,类型检查器将发出警告。

用 JSDoc 类型表示类型信息

用 JSDoc 类型表示类型信息:

  • JSDoc 是一种注释语法,它允许你在 JavaScript 文件中添加信息来描述代码的结构,包括类型信息。虽然 TypeScript 主要是为了在 .ts 文件中使用,但它能够解析 .js 文件中的 JSDoc 注释以提供类型检查和智能提示。

  • 通过在函数、变量或者类上方写上特殊格式的注释,你可以告诉 TypeScript 编译器这段代码期望的数据类型是什么。这个过程不需要将文件转换成 .ts 文件,便于那些希望在现有的 JavaScript 项目中逐渐引入类型检查的用户。

  • 使用 JSDoc 时,你需要在注释中使用特定的标签,如 @param 来描述函数参数的类型,或者 @type 来描述变量的类型。

例子:

/** @type {number} */
var x;

x = 0; // 正确
x = "hello"; // TypeScript 将会警告你,字符串不是 number 类型。

/**
 * 加法函数
 * @param {number} a 第一个加数
 * @param {number} b 第二个加数
 * @returns {number} 返回两个加数的和
 */
function add(a, b) {
  return a + b;
}

add(1, 2); // 正确
add("1", "2"); // TypeScript 将会警告你,参数应该是 number 类型。
  • 当你运行 TypeScript 的类型检查器时,即使在 .js 文件中,TypeScript 也会读取这些 JSDoc 注释,并根据你提供的信息对代码进行类型检查。

  • JSDoc 对于渐进式地在 JavaScript 项目中引入 TypeScript 或改善现有 JavaScript 代码的编辑器支持非常有用,尤其是当全面迁移到 TypeScript 代码库不切实际或不可行时。

属性的推断来自于类内的赋值语句

属性的推断来自于类内的赋值语句指的是在 TypeScript 中,类的属性类型可以通过类内部的赋值语句自动推断出来。这意味着当你在类内部给属性赋初值时,TypeScript 能够根据你赋的值判断出属性应该是什么类型。

  • 假如有一个类 Person,它有一个成员变量 name,你在构造函数或者其他地方给 name 赋了一个字符串值,TypeScript 会推断 name 的类型为 string

    class Person {
      name = "Alice"; // TypeScript 推断 name 是 string 类型
    }
    
  • 如果你给属性赋了一个数字,TypeScript 会推断这个属性的类型为 number

    class Counter {
      count = 0; // TypeScript 推断 count 是 number 类型
    }
    
  • 当属性没有在声明时直接初始化,并且也没有在构造函数中明确初始化时,如果开启了 TypeScript 的严格属性初始化检查(strictPropertyInitialization),编译器会抛出错误,提示需要初始化属性或者在属性名前加上!来告知 TypeScript 该属性会被稍后分配。

    class Game {
      score: number; // 如果开启了严格属性检查,这会报错因为 score 没有被初始化
      constructor() {
        this.score = 0; // 在构造函数中初始化属性
      }
    }
    
  • 如果你在类中对属性进行了多次不同类型的赋值操作,TypeScript 会根据所有赋值操作推断出一个兼容所有赋值的类型(通常是这些类型的联合类型)。

    class DataHolder {
      data: string | number;
      constructor() {
        this.data = "Initial data"; // 第一次赋值为 string 类型
        this.data = 42; // 第二次赋值为 number 类型
      }
    }
    

注意,在没有初始化的情况下,如果你没有显式指定类型,TypeScript 可能无法自动推断类型,这时候你需要手动指定类型或者初始化属性以确保类型安全。

构造函数等同于类

构造函数等同于类的概念是说,在 JavaScript 中,你可以使用构造函数来创建具有相似特性和行为的多个对象实例。在引入了 ES6 的class关键字前,这种通过构造函数来模拟“类”的行为是非常常见的。TypeScript 作为 JavaScript 的超集,支持新的class语法,并能对其进行类型检查和补充特性。

以下是一些关于构造函数和类的关键点及示例:

  • 在 JavaScript ES5 中,通常使用函数和原型链来实现类和继承。

    function Car(make, model) {
      this.make = make;
      this.model = model;
    }
    
    Car.prototype.honk = function () {
      console.log("Beep beep!");
    };
    
    var myCar = new Car("Toyota", "Corolla");
    myCar.honk(); // 输出: Beep beep!
    
  • 在 ES6 及更高版本的 JavaScript 中,可以使用class关键字来定义类。

    class Car {
      constructor(make, model) {
        this.make = make;
        this.model = model;
      }
    
      honk() {
        console.log("Beep beep!");
      }
    }
    
    let myCar = new Car("Toyota", "Corolla");
    myCar.honk(); // 输出: Beep beep!
    
  • TypeScript 中的类可以包含显式类型注解,提供更好的类型安全。

    class Car {
      make: string;
      model: string;
    
      constructor(make: string, model: string) {
        this.make = make;
        this.model = model;
      }
    
      honk(): void {
        console.log("Beep beep!");
      }
    }
    
    let myCar: Car = new Car("Toyota", "Corolla");
    myCar.honk(); // 输出: Beep beep!
    
  • TypeScript 的类还支持修饰符如public, private, 和protected来限制成员的访问性。

    class Car {
      private make: string;
      private model: string;
    
      constructor(make: string, model: string) {
        this.make = make;
        this.model = model;
      }
    
      public honk(): void {
        console.log("Beep beep!");
      }
    }
    

理解 JavaScript 中构造函数和类的等效性,可以帮助刚从 JavaScript 过渡到 TypeScript 的开发者更容易地理解 TypeScript 中类的概念,以及如何用 TypeScript 来增强这些结构的类型安全性和面向对象编程能力。

支持 CommonJS 模块

支持 CommonJS 模块

  • CommonJS 是一种在 JavaScript 中使用的模块标准,特别是在 Node.js 环境中。
  • 一个 CommonJS 模块通常使用 require 来导入其他模块,并用 module.exportsexports 来导出模块。
  • TypeScript 支持对这种类型的模块进行类型检查,从而确保你导入的变量或函数正确地对应了它们实际的类型。

举例:

  1. 假设有一个 CommonJS 模块,名为 mathUtils.js,其中包含了一个加法函数:
// mathUtils.js (JavaScript 文件)
function add(x, y) {
  return x + y;
}

module.exports = { add };
  1. 在 TypeScript 文件中导入并使用此模块时,可以像使用普通 TypeScript 模块一样享受类型检查:
// calculator.ts (TypeScript 文件)
import { add } from "./mathUtils";

// 正确使用
const result: number = add(10, 20); // TypeScript 知道 result 是 number 类型

// 错误使用将会得到 TypeScript 错误提示
const wrongResult: string = add(10, 20); // Error: Type 'number' is not assignable to type 'string'.
  1. 如果在 mathUtils.js 中没有明确指定导出的类型,TypeScript 将推断其类型。我们也可以通过声明文件(*.d.ts)来提供显式的类型信息。

例如,你可以为 mathUtils.js 创建一个 mathUtils.d.ts 声明文件:

// mathUtils.d.ts
export function add(x: number, y: number): number;

通过这种方式,TypeScript 编译器能够理解 add 函数接受两个 number 类型的参数,并返回一个 number 类型的结果,进一步提高代码的类型安全性。

  • 通过 TypeScript 的类型检查,可以减少因类型错误导致的运行时错误,让 JavaScript 开发者更顺畅地迁移到 TypeScript。
  • TypeScript 的配置文件 tsconfig.json 中的 allowJs 选项允许在 TypeScript 项目中包含 JavaScript 文件,并进行类型检查。

类,函数和对象字面量是命名空间

  • TypeScript 增强了 JavaScript 的类型系统,引入了类型检查这个概念。当你在 TypeScript 中使用类、函数和对象字面量时,它们在自己的作用域中可以充当“命名空间”。

  • 命名空间是一种包含特定代码(如变量、接口、类等)的方式,防止不同部分的代码之间发生名称冲突。

  • 类型检查意味着 TypeScript 会在编译时验证变量和参数的类型,确保它们符合预期。

例如,考虑以下几点:

// 类作为命名空间
class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  greet() {
    return `Hello, my name is ${this.name}!`;
  }
}

let person = new Person("Alice");
console.log(person.greet()); // 输出: Hello, my name is Alice!

// 函数作为命名空间
function multiply(a: number, b: number): number {
  return a * b;
}

let result = multiply(2, 4);
console.log(result); // 输出: 8

// 对象字面量作为命名空间
let shapes = {
  square: { length: 10 },
  rectangle: { width: 10, height: 20 },
};

function getSquareArea(square: { length: number }) {
  return square.length * square.length;
}

console.log(getSquareArea(shapes.square)); // 输出: 100
  • 在这些例子中,Person 类、multiply 函数和 shapes 对象字面量都提供了一个容器来封装相关数据和功能,有助于组织代码,并通过类型检查来提高代码的稳健性和可维护性。

对象字面量是开放的

在 TypeScript 中,当你使用 JavaScript 文件进行类型检查时,需要理解对象字面量的特性。其中一个特点是它们通常被认为是"开放的"或"扩展的"。这意味着:

  • TypeScript 允许在对象字面量中包含额外的属性,即便这些属性没有在预期的类型定义中显式声明。

举个例子,假设我们有以下接口:

interface Person {
  name: string;
  age: number;
}

如果你尝试创建一个Person类型的对象,并给它添加一个不在Person接口中声明的属性,如下所示:

let person: Person = {
  name: "Alice",
  age: 25,
  phone: "+12345678", // 这个属性在Person接口中没有声明
};

在一个纯 TypeScript 环境中,这段代码会产生错误,因为phone属性在Person接口中未被定义。但是,在一个 JavaScript 文件中启用了 TypeScript 的类型检查情况下(比如通过在文件开头加上// @ts-check注释),该错误可能不会被报出,因为对象字面量默认是开放的。

另一方面,如果你显式地将对象字面量赋值给一个类型注解变量,则 TypeScript 会进行额外属性检查。在这种情况下,上面的例子就会产生一个错误,因为person变量的类型被注解为Person接口,它不包括phone属性。

要解决这个问题,你可以:

  • 更新Person接口,包含所有需要的属性。
  • 使用索引签名来表示Person可以有任意数量的其他属性。

例如,以下方式允许Person接口有任何字符串作为键的额外属性:

interface Person {
  name: string;
  age: number;
  [propName: string]: any; // 索引签名
}

let person: Person = {
  name: "Alice",
  age: 25,
  phone: "+12345678", // 现在这没问题了,因为有了索引签名
};

总结一下,"对象字面量是开放的"这个概念对于从 JavaScript 过渡到 TypeScript 的学习者来说很重要,因为它涉及到 TypeScript 如何处理多余的属性和类型安全性。理解这一点有助于编写更严格的类型定义并避免潜在的类型错误。

null,undefined,和空数组的类型是 any 或 any[]

在 TypeScript 中,如果你有一个 JavaScript 文件并且正在使用 TypeScript 进行类型检查(通过 allowJscheckJs 选项开启),那么 TypeScript 需要对你现存的 JavaScript 代码尽可能地推断出类型信息。

  • nullundefined 是 JavaScript 中的两个原始数据类型,它们各自只有一个值:nullundefined。在 TypeScript 中,这两者也被认为是他们各自类型的唯一成员。

    let a = null; // a 的类型是 any
    let b = undefined; // b 的类型是 any
    

    这里的 any 类型是 TypeScript 特有的类型系统中的一部分,表示一个变量可以是任何类型。在 JavaScript 文件的类型检查中,未经声明的类型默认是 any

  • 空数组在没有显式类型注释的情况下,会被推断为 any[] 类型,即一个包含任意类型元素的数组。

    let c = []; // c 的类型是 any[]
    c.push(1); // 正确
    c.push("string"); // 正确
    

    上面的例子中,数组 c 被推断为 any[] 类型,因此你可以向其中添加任意类型的元素,不会有类型错误。

总结起来,在 JavaScript 文件中进行类型检查时,默认情况下,TypeScript 会给 nullundefined 以及空数组赋予 anyany[] 类型。这种设计上的选择是为了提高现有 JavaScript 项目向 TypeScript 迁移时的灵活性和容错性。然而,使用 any 类型过多会失去 TypeScript 提供的静态类型检查的很多好处。因此,在实际开发中,应该尽量避免使用 any 类型,而是给变量提供明确的类型注释。

函数参数是默认可选的

在 JavaScript 中,函数参数默认是可选的,这意味着即使你没有明确地标记一个参数为可选(使用?),调用者也可以选择不传递这个参数,而不会导致错误。但这在 TypeScript 中是不同的。TypeScript 带来了类型安全性,要求我们更明确地表达我们的意图。下面通过几个实例来解释这一点。

  • JavaScript 行为

    function greet(name) {
      console.log(`Hello, ${name}!`);
    }
    
    greet(); // 输出: "Hello, undefined!"
    

    在这个 JavaScript 示例中,尽管greet函数期望有一个name参数,但调用它时没有传入任何参数也不会引发错误,只是name在函数体内的值为undefined

  • TypeScript 默认行为

    function greet(name: string) {
      console.log(`Hello, ${name}!`);
    }
    
    greet(); // TypeScript编译时报错:Expected 1 arguments, but got 0.
    

    相对于 JavaScript,上述 TypeScript 代码在尝试以同样的方式调用greet函数时会在编译阶段报错,因为我们没有提供必需的name参数。

  • 在 TypeScript 中显式声明可选参数

    function greet(name?: string) {
      console.log(`Hello, ${name ? name : "stranger"}!`);
    }
    
    greet(); // 正常运行,输出: "Hello, stranger!"
    

    在这个 TypeScript 示例中,通过在参数名name后面加上?来标记它为可选,这允许我们在不提供name参数的情况下调用greet函数,同时避免了编译时错误。如果name被省略,其值将为undefined,我们可以通过条件操作符(name ? name : "stranger")来提供一个默认的问候。

通过理解和利用 TypeScript 的可选参数特性,你可以构建更健壮、易于理解和维护的接口和函数,同时享受 TypeScript 静态类型检查所带来的好处。

由 arguments 推断出的 var-args 参数声明

由 arguments 推断出的 var-args 参数声明

  • 在 JavaScript 中,arguments 是一个类数组对象,它包含了函数被调用时传入的所有参数。
  • TypeScript 能够从这个 arguments 对象推断出变长参数列表(也就是所谓的 var-args),即使在 .js 文件中,如果你启用了 TypeScript 的类型检查。

举例来说:

// JavaScript (.js) 文件中的代码
function concatenateAll() {
  let result = "";
  for (let i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
}

// 上面的函数可以接收任意数量的参数,并将它们连接成一个字符串返回。
  • 当你在 TypeScript 环境中使用上述 JavaScript 代码时,TypeScript 会尝试根据 arguments 的使用推断出函数参数的类型。

例如,如果在.ts 文件中对上述 JavaScript 函数进行类型检查:

// 被 TypeScript 类型检查的 JavaScript 代码
const result = concatenateAll("Hello, ", "TypeScript ", "world!");

// TypeScript 将推断 `concatenateAll` 的参数为一系列的任意类型,
// 因为它看到 `arguments` 被用于字符串连接操作。
  • 虽然在原生 JavaScript 中我们没有显式地声明参数类型,但是在 TypeScript 中,通过分析 arguments 的用法,TypeScript 能够推断出期望的参数类型应当是类似 (string, string, ...) 这样可以被连接的类型。

  • 这种由 arguments 推断出的参数类型有助于在不改变原始 JavaScript 代码结构的情况下提供类型安全,因此当 .js 文件被包含在 TypeScript 项目中时,它们仍可以受益于类型检查。

  • 需要注意的是,虽然 TypeScript 可以进行这样的推断,最佳实践还是建议在使用 TypeScript 时明确地为函数参数定义类型,这样可以更好地利用 TypeScript 的类型系统,获得更稳定、可读性更高的代码。

未指定的类型参数默认为 any

未指定的类型参数默认为 any

  • 在 TypeScript 中,当你使用泛型编程时,有时可能会遇到函数或类没有明确指出其类型参数的情况。

  • 泛型允许你创建可以支持多种数据类型的组件。比如,你可以有一个数组,它可以包含任何类型的元素,从数字到字符串。

  • 当你不指定一个泛型的具体类型时,TypeScript 的默认行为是将这个类型参数视作 any 类型。

  • any 类型在 TypeScript 中就像是一个逃生舱,它几乎没有类型检查。这意味着如果你使用 any,你基本上放弃了 TypeScript 提供的类型安全保障。

  • 这种默认行为可能会带来隐患,因为编译器不会提醒你关于类型错误或者潜在问题,这和 JavaScript 的行为相似。

例子:

function identity<T>(arg: T): T {
  return arg;
}

let output = identity("myString"); // 类型参数被指定为 string
let outputAny = identity("myString"); // 类型参数不指定,默认为 any
  • 在第二个 identity 调用中,即使我们传入了一个字符串,因为我们没有指定类型 <T>,所以 TypeScript 默认此时 Tany 类型。

  • 使用 --noImplicitAny 编译选项可以避免这种情况,它会在你忘记提供泛型类型参数时给出编译错误。

  • 最佳实践是尽量显式指定所有类型参数,以利用 TypeScript 的类型系统。

在 extends 语句中:

在 TypeScript 中,“未指定的类型参数默认为 any” 和 “在 extends 语句中” 是两个重要的概念,尤其是对于刚学习 TypeScript 的 JavaScript 开发者来说。下面我会分别讲解这两个概念,并提供一些例子。

  • 未指定的类型参数默认为 any

    • 当你使用泛型(Generics)但没有明确指定类型参数时,TypeScript 默认将类型参数视为 any 类型。

    • 这意味着,如果你不指定具体的类型,TypeScript 不会进行严格的类型检查,从而提供了灵活性,但同时减少了类型安全性。

    • 例如:

      function identity(value) {
        return value;
      }
      

      上面的代码中,identity 函数的参数 value 没有指定类型,因此它会被推断为 any 类型。

      使用泛型改写后:

      function identity<T>(value: T): T {
        return value;
      }
      let result = identity("myString");
      

      在这个例子中,若调用 identity 函数时不传入类型参数,T 将默认为 any 类型。但是这里我们通过传递 "myString" 让 TypeScript 自动推断出 Tstring 类型。

  • 在 extends 语句中

    • TypeScript 允许类和接口使用 extends 关键字来实现继承和扩展功能。

    • 这里的关键点是理解 TypeScript 如何处理类型的继承,尤其是当涉及到复杂类型或泛型时。

    • 例如:

      interface Shape {
        draw(): void;
      }
      
      interface Colorful extends Shape {
        color: string;
      }
      

      在这个例子中,Colorful 接口通过使用 extends 关键字继承了 Shape 接口。这意味着任何实现 Colorful 接口的对象都必须具有 draw 方法和 color 属性。

      另一个例子,使用类:

      class Animal {
        name: string;
        constructor(name: string) {
          this.name = name;
        }
        move(distanceInMeters: number = 0) {
          console.log(`${this.name} moved ${distanceInMeters}m.`);
        }
      }
      
      class Snake extends Animal {
        constructor(name: string) {
          super(name);
        }
        move(distanceInMeters = 5) {
          console.log("Slithering...");
          super.move(distanceInMeters);
        }
      }
      

      这里,Snake 类通过 extends 关键字继承了 Animal 类。这意味着 Snake 类不仅继承了 Animal 类的属性和方法,还可以添加或重写自己的方法,比如 move 方法。

理解这些概念对于深入学习 TypeScript 非常重要,它们帮助我们构建更加健壮和可维护的代码。

在 JSDoc 引用中:

在 TypeScript 的 JSDoc 注解中,当你使用泛型但没有指定具体的类型参数时,默认情况下这些类型参数会被视为 any 类型。这表示函数或者变量将能接受任何类型的值,失去了 TypeScript 强类型检查的优势。

举个例子来说:

  • 假设你有一个 JavaScript 函数,你希望它能够处理数组,并返回第一个元素。如果你不用 JSDoc 或 TypeScript 类型注解,那么 TypeScript 不会对这个数组里元素的类型进行检查。
/**
 * @param {Array} array - 一个数组
 * @returns 返回数组中的第一个元素
 */
function getFirstElement(array) {
  return array[0];
}
  • 在以上代码中,尽管我们使用了 JSDoc 来注释 array 参数应当是一个数组(Array),我们并没有指定数组中元素的类型。由于 TypeScript 默认情况下将未指定类型的泛型参数解释为 any,这意味着你可以传入任何类型的数组。

  • 如果想要确保类型安全,我们应该在 JSDoc 中指定泛型参数的类型。例如,如果我们希望数组只包含数字,我们可以这么写:

/**
 * @param {Array<number>} array - 一个数字数组
 * @returns 返回数组中的第一个数字
 */
function getFirstElement(array) {
  return array[0];
}
  • 这样 TypeScript 就知道传给 getFirstElement 函数的 array 应该是一个数字数组。如果你尝试传入一个字符串数组,TypeScript 编译器将会警告你类型不匹配。

通过在 JSDoc 中明确地指定类型参数,你可以让 TypeScript 更好地帮助你进行类型检查和自动补全等功能,从而使得代码更加健壮和易于维护。

在函数调用中

未指定的类型参数默认为 any

在 TypeScript 中,泛型是允许同一个函数接受不同类型参数的一种工具。但有时候,在调用一个使用了泛型的函数时,可能会忘记传入类型参数。这时,默认情况下,TypeScript 会将这些未指定的类型参数视为 any 类型。

  • 当你不明确指定泛型类型参数时,该参数会被推断为 any 类型。
  • 使用 any 类型会导致 TypeScript 编译器失去对类型的检查能力,因此尽量避免。
  • 在严格模式下(--strict 标志开启),编译器会警告未指定的类型参数。

例子:

function identity<T>(arg: T): T {
  return arg;
}

// 明确指定了类型参数为 number
let output1 = identity<number>(5);

// 没有指定类型参数,TypeScript 将类型参数推断为 any
let output2 = identity(5);

// 在严格模式下,第二种调用会产生警告信息,因为我们没有明确地指定类型参数

在第一个调用中,我们明确传递了 number 作为类型参数,所以 T 被当做 number 处理。在第二个调用中,我们没有传递类型参数,所以 TypeScript 默认将 T 推断为 any 类型。这意味着 output2 的类型实际上是 any,我们失去了类型检查的优势。

为了保持代码的健壮性,最好显式提供类型参数或利用 TypeScript 的类型推断功能,让编译器帮助我们确定正确的类型。

支持的 JSDoc

支持的 JSDoc

TypeScript 不仅可以在.ts 文件中工作,也能在.js 文件中通过 JSDoc 注释来提供类型信息。这样做允许你在不完全迁移到 TypeScript 的情况下,在普通的 JavaScript 项目中逐渐引入类型检查。以下是一些常用的 JSDoc 注解和它们的应用示例:

  • @type:用于声明变量或表达式的类型。

    /** @type {number} */
    var x;
    x = 0; // 正确
    x = "hello"; // 在启用了JS文件类型检查的情况下,TypeScript会报错
    
  • @param:用于声明函数参数的类型。

    /**
     * @param {string} name
     */
    function greet(name) {
      console.log("Hello, " + name.toUpperCase() + "!!");
    }
    greet(42); // TypeScript会报类型错误
    
  • @returns(或 @return):用于指明函数返回值的类型。

    /**
     * @returns {number}
     */
    function add(x, y) {
      return x + y;
    }
    const result = add(5, "test"); // TypeScript会提示类型错误
    
  • @typedef:用于定义一个类型,之后可以像使用其它类型一样使用它。

    /**
     * @typedef {Object} SpecialDate - 特殊日期对象
     * @property {number} year 年份
     * @property {number} month 月份
     * @property {number} day 日期
     */
    
    /** @type {SpecialDate} */
    let myBirthday;
    
    myBirthday = {
      year: 1995,
      month: 12,
      day: 17,
    };
    

利用这些 JSDoc 注解,即便是在纯 JavaScript 项目中,也能享受到 TypeScript 带来的类型检查优势。对于初学者来说,这是一个很好的过渡方式,既可以熟悉类型系统的思维方式,又不必立即全面转向 TypeScript。

@type

  • @type 是 JSDoc 注释中用来指定变量或表达式类型的标签。通过使用这个标签,即使在 JavaScript 文件中,你也能享受到 TypeScript 提供的类型检查功能。这对于逐步迁移到 TypeScript 或者在不完全转向 TypeScript 的项目中希望利用类型检查的情况非常有用。

  • 例子 1:

    // @ts-check
    /** @type {number} */
    var x;
    x = 0; // 正确
    x = "hello"; // 将会报错,因为 x 应该是一个数字
    

    在这个例子中,通过在文件顶部添加 // @ts-check 开启了 TypeScript 的类型检查功能(即使这是一个 JS 文件)。然后,我们使用 @type 来指明变量 x 应该是一个 number 类型。当尝试将字符串 "hello" 赋值给它时,我们将得到一个错误提示,因为这与我们声明的类型不匹配。

  • 例子 2:

    // @ts-check
    /**
     * Adds two numbers.
     * @param {number} a 第一个数
     * @param {number} b 第二个数
     * @returns {number}
     */
    function add(a, b) {
      return a + b;
    }
    add(5, "test"); // 将会报错,因为第二个参数应该是一个数字
    

    在这个例子中,我们定义了一个函数 add,它接受两个参数 ab。通过 @param 标签,我们指定了这两个参数都应该是 number 类型。同样地,我们用 @returns 标签指定了函数返回值的类型。如果尝试传递不符合这些类型的参数,比如在调用 add(5, "test") 时,将会得到一个错误提示。

  • 使用 @type 可以帮助在不完全迁移到 TypeScript 的情况下,在 JS 项目中引入类型检查,提高代码质量和可维护性。此外,它也是一个很好的学习工具,帮助初学者逐渐熟悉类型系统和类型注解的方式。

转换

Typescript 支持 JSDoc 注释来为 JavaScript 文件提供类型信息。@type 是 JSDoc 中一个常用的标签,它可以指定一个变量或表达式的类型。在 TypeScript 中使用 @type 可以帮助你在纯 JavaScript 项目中引入类型检查,而不必完全转换为 TypeScript。

下面是一些使用 @type 的例子:

  • 指定变量类型
// @ts-check
/** @type {number} */
var myNumber;
myNumber = 10; // 正确
myNumber = "hello"; // 类型检查错误,因为 'hello' 不是 number 类型
  • 指定函数参数类型和返回值类型
// @ts-check
/**
 * @param {string} name
 * @returns {string}
 */
function greet(name) {
  return `Hello, ${name}!`;
}
greet(123); // 类型检查错误,因为参数应该是 string 类型
  • 使用对象类型
// @ts-check
/**
 * @type {{name: string, age: number}}
 */
var person;
person = { name: "Alice", age: 25 }; // 正确
person = { name: "Alice", age: "twenty-five" }; // 类型检查错误,age 应该是 number 类型
  • 在数组中使用类型
// @ts-check
/** @type {Array<number>} */
var numbers;
numbers = [1, 2, 3]; // 正确
numbers = [1, "two", 3]; // 类型检查错误,数组元素应该是 number 类型

利用 @type 标签能够提高代码质量,通过显式声明变量、函数参数和返回值的类型,使得代码更易于理解和维护。同时,在使用第三方库时,也能够帮助预防潜在的类型错误。

导入类型

TypeScript 支持在 JavaScript 文件中通过 JSDoc 注释来添加类型信息。@type 标签允许你指定一个变量或表达式的类型。当你想要导入一个类型来用于你的 JSDoc 注释时,可以使用 TypeScript 的类型导入语法来完成。

以下是如何使用 @type 来导入和应用类型的一些例子:

  • 导入类型并在变量上使用它:

    // @ts-check
    
    /**
     * @typedef {import("./some-module.js").SomeType} SomeType
     */
    
    /** @type {SomeType} */
    var myVar;
    

    在这个例子中,我们首先使用 @typedefimport 类型导入语法从另一个模块导入了 SomeType 类型。然后,我们使用 @type 将这个导入的类型指定给 myVar 变量。

  • 应用到函数参数和返回类型:

    // @ts-check
    
    /**
     * @typedef {import("./another-module.js").AnotherType} AnotherType
     */
    
    /**
     * @param {AnotherType} param
     * @returns {AnotherType}
     */
    function myFunction(param) {
      // 函数逻辑...
      return param;
    }
    

    这里我们定义了一个名为 myFunction 的函数,其参数和返回值的类型都指定为从另一个模块导入的 AnotherType

注意,使用 @type 和类型导入功能需要开启 TypeScript 的类型检查,即在文件顶部添加 // @ts-check 注释。而且,这些 JSDoc 注释对有基本 JavaScript 知识的开发者来说是相对直观易懂的,能够帮助你平滑地过渡到 TypeScript 的类型系统。

@param 和@returns

TypeScript 提供了在 JavaScript 文件中通过 JSDoc 注释添加类型信息的能力。这对于那些刚从 JavaScript 迁移到 TypeScript 或者在不完全转换为 TypeScript 的情况下使用一些 TypeScript 特性的用户来说是非常有用的。

  • @param

    • @param 标签用来描述函数或方法参数的类型。
    • 在参数名前可以指定其类型,让 TypeScript 理解并检查该类型。
    • 如果你在 JavaScript 中使用了 JSDoc 注释,TypeScript 就会考虑这些信息进行类型检查,即使你没有将文件扩展名改为 .ts

    例子:

    /**
     * Adds two numbers together.
     * @param {number} a The first number.
     * @param {number} b The second number.
     * @returns {number} Sum of a and b.
     */
    function add(a, b) {
      return a + b;
    }
    
  • @returns

    • @returns(或者 @return,二者等价)标签用来描述函数或方法返回值的类型。
    • 它后面跟着类型描述,说明函数结束时应该返回什么类型的值。

    例子:

    /**
     * Multiplies two numbers.
     * @param {number} a The first number.
     * @param {number} b The second number.
     * @returns {number} Product of a and b.
     */
    function multiply(a, b) {
      return a * b;
    }
    

使用 @param@returns 可以帮助你明确地说明函数接收什么类型的参数以及返回什么类型的结果。这对代码的可读性和维护性有很大帮助,并且 TypeScript 能够利用这些信息提供更好的类型检查和自动补全功能。即使你现在还主要写 JavaScript,习惯了这种注释方式也会让未来可能的迁移到 TypeScript 更加平滑。

@typedef, @callback, 和 @param

JSDoc 是一种注释语法,用于给 JavaScript 代码添加文档说明。TypeScript 能够利用这些注释来提供类型检查和智能感知的功能。以下是对@typedef@callback@param标签的解释:

  • @typedef

    • 用于定义一个复杂的对象类型或别名,使得你可以在其他地方重复使用。

    • 示例:

      /**
       * @typedef {Object} User
       * @property {string} name 用户名
       * @property {number} age 年龄
       */
      
      /** @type {User} */
      var user = {
        name: "张三",
        age: 30,
      };
      
  • @callback

    • 用于定义回调函数的类型,这让你可以指定回调函数参数和返回值的类型。

    • 示例:

      /**
       * @callback ComputeSumCallback
       * @param {number} error 错误信息
       * @param {number} result 计算结果
       */
      
      /**
       * @param {number[]} numbers 要相加的数字数组
       * @param {ComputeSumCallback} cb 回调函数
       */
      function computeSum(numbers, cb) {
        // ...计算过程
        let err = null;
        let result = 0;
        try {
          result = numbers.reduce((a, b) => a + b, 0);
        } catch (error) {
          err = error;
        }
        cb(err, result);
      }
      
  • @param

    • 在函数或方法的注释中用来描述一个参数的类型、名称和描述。
    • 可以为每个参数添加多个@param标签来提供更详细的信息。
    • 示例:
      /**
       * 将两个数字相加
       * @param {number} num1 第一个加数
       * @param {number} num2 第二个加数
       * @returns {number} 两个加数的和
       */
      function add(num1, num2) {
        return num1 + num2;
      }
      

通过这些 JSDoc 注释,即便在常规的 JavaScript 文件中,也能享受到 TypeScript 类型系统带来的好处,如类型检查和自动补全,而不需要完全转换代码到 TypeScript。这对于刚开始学习 TypeScript 的 JavaScript 开发者非常有用,因为它们可以逐步地迁移到 TypeScript。

@template

@template 标签在 JSDoc 中用于声明一个函数或者方法是泛型的。泛型允许你定义一个函数、接口或类的时候不具体指定它操作的数据类型,可以在使用时再确定具体的类型。这样做的好处是增加了代码的复用性和灵活性。

使用 @template 的例子

  • 基本使用:

    /**
     * @template T
     * @param {T} data
     * @returns {T}
     */
    function identity(data) {
      return data;
    }
    
    // 使用函数
    const numberResult = identity(5); // 类型为number
    const stringResult = identity("hello"); // 类型为string
    

    这个例子中,identity 函数是一个泛型函数,通过 @template T 声明了一个名为 T 的泛型。这意味着该函数可以接受任何类型的 data 参数,并返回相同类型的数据。

  • 在类中使用:

    /**
     * 表示一个容器,可以存储任何类型的值
     * @template T
     */
    class Container {
      /**
       * @param {T} initialData
       */
      constructor(initialData) {
        /** @type {T} */
        this.data = initialData;
      }
    
      /**
       * 获取存储的数据
       * @returns {T}
       */
      getData() {
        return this.data;
      }
    
      /**
       * 设置存储的数据
       * @param {T} newData
       */
      setData(newData) {
        this.data = newData;
      }
    }
    
    // 使用Container类
    const numberContainer = new Container(123);
    const stringContainer = new Container("Hello");
    

    在这个例子中,Container 类通过 @template T 成为了一个泛型类,能够处理不同类型的数据。构造函数接收初始数据 initialData,并且类内部的方法也都能正确地处理这个类型 T 的数据。

结论

使用 @template 标签和泛型可以大大提高 JavaScript 代码的灵活性和复用性。尤其对于一些需要处理多种数据类型的工具函数或组件来说,泛型提供了一种安全且有效的方式来编写更加通用的代码。

@constructor

@constructor 是一个 JSDoc 注释标签,用于在 JavaScript 中指明一个函数是构造函数。TypeScript 可以通过 JSDoc 注释了解 JavaScript 代码的意图,并且使用这些信息进行类型检查。

  • 当你在一个函数上方使用 @constructor 标签时,你告诉 TypeScript 这个函数应该被作为构造函数来调用,即使用 new 关键字来创建对象实例。
  • 这可以帮助 TypeScript 确定哪些函数被设计为构造器,即便在不将代码转换成 TypeScript 的情况下也能享受到类型检查的好处。

举例说明:

/**
 * @constructor
 */
function Person(name) {
  this.name = name;
}

var person = new Person("Alice");

在上面的代码中:

  • 使用 @constructor 标签注释 Person 函数,表明它是一个构造函数。
  • 之后我们使用 new 创建了一个 Person 的实例,并将其赋给 person 变量。

如果尝试以非构造函数的方式调用 Person(例如,直接调用 Person('Alice') 而不是 new Person('Alice')),TypeScript 将会警告开发者这可能是一个错误的使用方式(如果启用了对应的 JSDoc 类型检查)。

@this

在 TypeScript 中使用 JSDoc 注解来为 JavaScript 文件添加类型信息时,@this标签用于指定一个函数中this的类型。这对于在 JavaScript 中编写面向对象的代码尤其有用,因为它可以帮助 TypeScript 理解this关键字在特定上下文中代表的具体类型,从而提供更准确的类型检查和智能提示。

  • 基本用法:当你在一个函数内部使用this关键字,并且希望 TypeScript 知道this的确切类型时,你可以在函数的 JSDoc 注释中使用@this来指明。

    /** @this {HTMLElement} */
    function handleClick() {
      console.log(this.id); // 这里的`this`被视为HTMLElement
    }
    
    document.getElementById("myButton").addEventListener("click", handleClick);
    

    在这个例子中,我们告诉 TypeScript handleClick 函数中的 this 是一个 HTMLElement 类型。这样 TypeScript 就能提供关于this.id等属性的正确类型提示和检查。

  • 与回调函数一起使用:在使用回调函数时,如果你知道回调中的this将是什么类型,也可以使用@this来指定。

    /**
     * @this {HTMLCanvasElement}
     */
    function setupCanvas() {
      this.getContext("2d").fillStyle = "blue"; // TypeScript知道`this`是`HTMLCanvasElement`
    }
    
    const canvas = document.createElement("canvas");
    setupCanvas.call(canvas); // 使用call来明确`this`的类型
    

    这个例子展示了如何通过@this在不直接修改函数定义的情况下,通过.call()方法改变函数上下文(this)的类型。

使用@this标签能够使 TypeScript 更好地理解和检查基于this的代码,特别是在处理 DOM 元素和面向对象编程时。这为 JavaScript 开发者在不完全迁移到 TypeScript 的情况下,仍能享受到一定程度的类型检查和代码自动完成功能。

@extends

@extends 是 JSDoc 注解中用来指明一个类继承自另一个类的标记。在 JavaScript 文件中使用 TypeScript 进行类型检查时,你可以用它来告诉 TypeScript 一些类的继承信息。

JSDoc 是一种标记语言,用于为 JavaScript 代码编写文档注释。而在 TypeScript 中,即便是在纯 JavaScript 的代码中,也能够利用 JSDoc 注释来提供类型信息,这对于逐渐过渡到 TypeScript 或者希望在不完全转移到 TypeScript 的情况下获得某些类型检查的项目非常有用。

以下是如何使用 @extends 来表示类继承的例子:

  • 假设有一个基类 Animal
/**
 * Represents an animal.
 */
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}
  • 现在你想创建一个名为 Dog 的新类,它继承自 Animal 类:
/**
 * Represents a dog, which is an animal.
 * @extends {Animal}
 */
class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}
  • 在上面的 Dog 类定义中,我们使用了 @extends {Animal} 来表明 Dog 类从 Animal 类继承。这允许 TypeScript 知道 Dog 类型的所有实例都将具备 Animal 类型的属性和方法。

  • 当在 JavaScript 文件中使用 TypeScript 类型检查(通常通过配置文件 jsconfig.jsontsconfig.json 启用)时,即使没有显式地使用 TypeScript 语法,TypeScript 编译器也会理解 @extends 并据此进行类型检查和智能提示。

请注意,JSDoc 注释必须紧跟在类声明或其他符号之前,否则 TypeScript 可能无法正确解析注释。

@enum

TypeScript 支持在 JavaScript 文件中通过 JSDoc 注释添加类型信息。@enum 是 JSDoc 中的一个标签,用于指示一个对象是一个枚举,并描述其可能的值。在 TypeScript 中,枚举是一种特殊的类型,它允许你为一组数值定义友好的名称。

使用 @enum 可以让你在纯 JavaScript 项目中利用 TypeScript 的类型检查器来检查那些伪装成枚举的普通对象。这样做可以增加代码的可读性和维护性。

下面是一个使用 @enum 的例子:

/**
 * @enum {number}
 */
const Direction = {
  Up: 1,
  Down: 2,
  Left: 3,
  Right: 4,
};

// 使用 Direction 枚举
const myDirection = Direction.Up;

在上面的例子中:

  • @enum {number} 通知 TypeScript,Direction 是一个包含数字类型值的枚举。
  • Direction 对象的属性 Up, Down, Left, Right 被视作枚举成员,每个成员都有对应的数字值。

当你在 .js 文件中使用 JSDoc 和 @enum 标签时,TypeScript 将能够提供关于这个枚举的类型信息,并帮助你在编码过程中避免错误,比如:

// 正确使用枚举成员
const newDirection = Direction.Left;

// 如果尝试使用未定义的枚举成员,TypeScript 将会警告
const wrongDirection = Direction.None; // 'None' does not exist on type 'Direction'

通过给 JavaScript 对象添加 @enum 类型,你可以模拟出 TypeScript 枚举的行为,并获得类型检查的好处,即使你的代码基础仍然是纯 JavaScript。

更多示例

Typescript 官方文档中的 "JavaScript 文件类型检查" 部分讲解了如何利用 JSDoc 注释来为 JavaScript 文件提供类型信息。在 "支持的 JSDoc" 小节下的 "更多示例" 子部分,通常会给出一些具体的代码示例,演示如何使用 JSDoc 来增强类型检查。

以下是一些 JSDoc 注释的基本应用方式及其对应的 TypeScript 类型检查效果的例子:

  • 函数参数类型与返回类型 使用 @param@returns 标签为函数的参数和返回值指定类型。

    /**
     * @param {number} a 第一个加数
     * @param {number} b 第二个加数
     * @returns {number} 两个加数的和
     */
    function add(a, b) {
      return a + b;
    }
    
  • 类和构造函数 使用 @class@constructor 标签定义一个类及其构造函数的类型。

    /**
     * @class
     */
    function Person(name) {
      /** @type {string} */
      this.name = name;
    }
    
    /** @type {Person} */
    const person = new Person("Alice");
    
  • 类型定义(Typedef) 使用 @typedef 创建自定义对象类型,并通过 @property 定义该类型的属性。

    /**
     * @typedef {Object} User
     * @property {string} name 用户名
     * @property {number} age 年龄
     */
    
    /**
     * @param {User} user 用户对象
     */
    function greet(user) {
      console.log(`Hello, ${user.name}!`);
    }
    
    /** @type {User} */
    const user = { name: "Bob", age: 30 };
    
  • 模块导出与导入 使用 @module 标记一个模块,然后用 @exports 表明哪些成员被导出。

    /**
     * @module myModule
     */
    
    /**
     * 这个函数会被导出
     * @returns {void}
     */
    function exportedFunction() {}
    
    /**
     * 私有函数不会被导出
     * @returns {void}
     */
    function privateFunction() {}
    
    module.exports.exportedFunction = exportedFunction;
    

通过这些 JSDoc 注释,在不转换为 TypeScript 文件的情况下,你依然可以在 JavaScript 文件中享受到 TypeScript 的类型检查功能。如果你使用的是 Visual Studio Code 或其他支持 TypeScript 的编辑器,它们会读取 JSDoc 注释并提供代码补全、错误提示等功能,帮助你写出更健壮的代码。

已知不支持的模式

在 TypeScript 中,使用 JSDoc 注释可以在 JavaScript 文件中加入 TypeScript 类型检查的功能。这种方法对于那些想要逐步迁移到 TypeScript 或希望在不完全切换到 TypeScript 的情况下利用其静态类型检查能力的开发者来说非常有用。

  • 已知不支持的模式指的是尽管 JSDoc 提供了丰富的类型信息描述能力,但仍然有一些特定的模式或语法结构,TypeScript 无法通过 JSDoc 注释来识别或支持。

示例和解释

以下是一些 TypeScript 官方文档中提到的、通过 JSDoc 不支持的模式的例子:

  • @template 标签 TypeScript 不支持在 JSDoc 中使用@template标签来定义泛型类型。虽然泛型是 TypeScript 中一项非常强大的特性,允许你创建可重用的组件或函数并同时保证类型安全,但目前通过 JSDoc 注释无法表达这种泛型关系。

  • 复杂的类型依赖 如果你的代码依赖于复杂的类型交叉或联合,并试图通过 JSDoc 注释来表达这些,可能会遇到困难。TypeScript 的类型系统非常强大,能够描述各种复杂的类型关系,例如通过&|表示的交叉类型和联合类型。但是,当这些关系变得特别复杂时,JSDoc 的表达能力可能就不足以捕获全部的类型信息了。

  • 某些高级类型和装饰器 TypeScript 支持高级类型特性,如映射类型、条件类型等,以及提供装饰器来修改类和方法的行为。这些高级特性无法通过 JSDoc 注释直接表达,因此在使用 JSDoc 进行类型检查时,这部分类型信息将会丢失。

结论

尽管 JSDoc 为 JavaScript 项目引入了一种类型检查的方法,让开发者能够在不完全迁移到 TypeScript 的情况下享受到类型安全的好处,但它的表达能力有限。某些复杂的类型表达、泛型定义以及高级类型特性无法通过 JSDoc 注释准确地表示。如果你的项目类型需求变得越来越复杂,可能需要考虑完全迁移到 TypeScript 以充分利用其类型系统的全部能力。