前端工程搭建:TypeScript类型约束

30 阅读7分钟

1. 为什么引入类型约束

在 JavaScript 中,由于其动态类型的特性,可能会导致一些难以察觉的错误,在大型项目中尤其是个问题。因此微软推出了 TypeScript,它在JavaScript的基础上提供了静态类型系统,因此可以在编译阶段进行类型检查,从而提前发现这类错误。TypeScript大大提升了代码的可读性和可维护性,目前已经成为了大型项目的标配。

  • TypeScript增强了开发工具的能力,结合类型系统可以更好地提供代码补全、文件引用跳转等功能
  • 通过类型定义起到了代码文档的作用,弥补了前端文档缺失的问题
  • TypeScript提供最新的JavaScript特性,开发人员可以使用更前沿的API能力

2. TypeScript基础

2.1 基础类型

TypeScript是JavaScript的超集,所以JavaScript的基础类型也适用于TypeScript。除此之外,它为了完善数据类型,还定义了以下基础类型:

  • never:表示那些永不存在的值的类型。它是所有类型的子类型,可以赋值给任何类型,但没有任何类型可以赋值给never(除了never自身)。通常用于表示一个函数总是抛出异常或者进入一个无限循环,导致无法正常返回一个有意义的值。
function raiseError(): never { throw new Error("资源不存在"); }
function infiniteLoop(): never { while (true) {  } }
  • any: any类型是 TypeScript 中比较特殊的一个类型,它可以代表任意类型的值。这意味着变量可以被赋予任何类型的值,并且可以对其进行任何操作,而不会受到类型检查的限制。使用any类型会失去 TypeScript 的类型安全优势,所以应该谨慎使用。它的使用场景:

    • 在与一些动态生成的数据或者从外部获取的数据交互时,无法确定数据的类型;
    • 使用any类型来兼容JavaScript旧代码
  • unknownunknown类型是 TypeScript 3.0 引入的新类型,它代表一个未知的类型。与any类型不同,unknown类型是类型安全的,在对unknown类型的值进行操作之前,必须先进行类型断言或者类型检查。这使得它在处理那些类型不确定的数据时,既能够提供一定的灵活性,又能保证类型安全。

  • voidvoid类型表示没有任何类型,通常用于函数没有返回值的情况。它与undefined有些类似,但void更强调函数的返回值类型为空。一个返回void类型的函数可以有return语句,但只能返回undefined或者不返回任何值。

  • enumenum枚举类型用于定义一组命名的常量集合。它允许开发人员使用更具语义的名称来表示一组相关的数值,使得代码更加可读和易于维护。枚举在处理一些具有固定选项或状态的值时非常有用。

  • tupletuple元组类型用于表示一个已知元素数量和类型的数组,其中每个元素的类型可以是不同的。相比于普通的数组类型(Array),它提供了一种更精确的方式来描述数组的结构,适用于那些元素类型和顺序有特定要求的场景。

2.2 高级类型

TypeScript通过组合基础类型,还可以提供高级类型,以处理更复杂的类型场景。

  • 泛型:泛型是 TypeScript 中一种强大的工具,它允许我们编写可以在多种类型上工作的代码,而不是为每个特定类型编写重复的代码。简单来说,泛型就像是一个 “占位符” 类型,在使用时可以被具体的类型替换。泛型在实际中被广泛应用,比如泛型函数、泛型类型等。
// 定义一个泛型函数,函数名为identity,它接受一个泛型参数T,参数arg的类型为T
function identity<T>(arg: T): T { return arg; } 
// 调用泛型函数时,指定T为具体的类型,这里指定为number类型 
let result1: number = identity<number>(5); 
// 也可以不手动指定类型,TypeScript会自动根据传入的参数推断出泛型参数的类型,这里推断为string类型 
let result2 = identity("hello"); 
  • extends:表示类型约束,可以用在很多不同的场景中

    • 继承:用于接口继承(Interface Inheritance)、类继承(Class Inheritance),可以使子类具有父类的属性和方法,并追加新的属性和方法。

    • 泛型约束(Generic Constraints):在泛型中,extends用于对泛型参数进行约束。它可以指定泛型参数必须满足的条件。通过泛型约束,可以在编写更通用的代码的同时,确保类型的安全性和合理性。

    interface KeyValuePair<T> { key: string; value: T; } 
    function printKeyValue<T, U extends KeyValuePair<T>>(obj: U): void {
        console.log(`键: ${obj.key}, 值: ${obj.value}`); 
    }
    
    • 条件类型(Conditional Types)extends用于判断一个类型是否满足某种条件,从而返回不同的类型,类似于 JavaScript 中的三元表达式。这种方式可以根据类型之间的关系来动态地确定一个类型,使得类型系统更加灵活和强大。比如,它可以与映射类型结合使用,构建更复杂的类型逻辑。

      interface OriginalObject { a: string; b: number; } 
      type StringToNumber<T> = { 
          // 如果属性值T[P]是string类型,就将其转换为number类型,否则保持不变。
          [P in keyof T]: T[P] extends string? number : T[P]; 
      }; 
      // 通过类型映射操作,生成新的类型NewObject
      type NewObject = StringToNumber<OriginalObject>; // { a: number; b: number; } 
      
  • 索引类型(Indexed Types):索引类型允许通过索引来访问对象的属性,并且可以对这些属性的类型进行约束和操作。

interface Person = { name: string; age: number; }
type Keys = keyof Person; // 'name' | 'age'
  • 交叉类型(Intersection Types):将多个类型合并为一个类型,新类型会同时拥有所有被合并类型的属性和方法。它使用&符号来表示。
interface HasName { name: string; } 
interface HasAge { age: number; } 
// 交叉类型Person同时具备HasName和HasAge的属性 
type Person = HasName & HasAge; // { name: string; age: number; }
  • 联合类型(Union Types):联合类型表示一个值可以是几种类型之一。它使用|符号来表示,用于处理可能是多种不同类型的数据。
type StringOrNumber = number | string;

3. 项目配置

3.1 安装依赖

首先需要安装 TypeScript 编译器,安装后就可以使用tsc命令进行编译。

# 本地安装:每个项目都有独立的编译器版本,避免不同项目之间的冲突
npm install --save-dev typescript

对于一些没有自带类型声明文件的 JavaScript 库,可能需要安装相应的@types/库来为其添加类型支持。这些类型声明文件可以帮助在使用第三方库时获得更好的类型检查和代码补全功能。

3.2 tsconfig.json配置

tsc运行时,TypeScript会在当前目录和父级目录中寻找tsconfig.json文件。这个文件是 TypeScript 项目的配置中心,用于告诉编译器如何处理项目中的 TypeScript 文件。

{
  "compilerOptions": {
      "target": "es5", // 编译后的JavaScript版本,es5兼容性强,能在多数旧浏览器运行;es6及以上适合现代环境
      "module": "commonjs", // 模块系统,commonjs用于Node.js,es6用于现代浏览器且支持模块特性
      "strict": true, // 开启严格模式,强制类型检查,减少隐式any等错误,提升代码质量
      "esModuleInterop": true, // 解决ES模块与CommonJS模块互操作问题,方便混用不同模块类型
      "sourceMap": true, // 生成sourceMap文件,便于调试时定位TypeScript原始代码位置
      "outDir": "dist", // 编译后文件输出目录
      "rootDir": "src", // 定义TypeScript源文件根目录,编译器从此处找.ts文件编译
      "baseUrl": ".", // 模块解析的基准路径,项目结构复杂时辅助定位模块
      // 配置模块别名路径,如@代表src,简化代码中模块导入路径
      "paths": {
          "@/*": [
              "src/*"
          ]
      }
  },
  "include": [
      // 包含需编译的文件,src下所有.ts及子目录下.ts文件都会被处理
      "src/**/*.ts"
  ],
  "exclude": [
      // 排除无需编译的文件,node_modules存放第三方库,test假设为测试文件,都不编译
      "node_modules",
      "test/**/*.ts"
  ]
}

3.3 Webpack 打包配置

  • 安装loader:为了处理 TypeScript 文件,需要安装ts-loaderawesome-typescript-loader
  • loder配置:对ts相关的文件进行配置
module.exports = {
  entry: "./src/index.ts",
  output: { filename: "bundle.js", path: path.resolve(__dirname, "dist") },
  module: {
    // 匹配`.ts`和`.tsx`文件
    rules: [{ test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ }],
  },
  // 解析模块时,优先查找`.tsx`、`.ts`和`.js`文件,这样在导入模块时可以省略文件扩展名
  resolve: { extensions: [".tsx", ".ts", ".js"] },
};