Skip to content

一. JavaScript 类型缺失

JavaScript 一门优秀的语言

任何新技术的出现都是为了解决原有技术的某个痛点。

JavaScript 是一门优秀的编程语言吗?

每个人可能观点并不完全一致,但是从很多角度来看,JavaScript 是一门非常优秀的编程语言, 而且,可以说在很长一段时间内这个语言不会被代替,并且会在更多的领域被大家广泛使用。

著名的 Atwood 定律:

  • Stack Overflow 的创立者之一的 Jeff Atwood 在2007年提出了著名的 Atwood 定律:any application that can be written in JavaScript, will eventually be written in JavaScript。
  • 任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现

其实我们已经看到了,这句话正在一步步被应验:

  • Web开发:Web 端开发几乎被 JavaScript 完全统治,HTML 和 CSS 作为静态内容展示,JavaScript 则负责交互、数据处理和逻辑控制。通过 React、Vue.js 等前端框架,开发者能够构建复杂而高效的 Web 应用。
  • 移动端开发:随着 React Native、Weex 和 UniApp 等框架的推出,JavaScript 已经能够跨平台开发移动应用,这意味着开发者可以通过一套代码实现 Android 和 iOS 的应用。微信小程序、支付宝小程序等也大量使用 JavaScript 来开发和运行。
  • 桌面端开发:桌面端应用的开发也被 JavaScript 渗透,Electron 框架允许开发者使用 JavaScript、HTML 和 CSS 来构建跨平台的桌面应用,例如 Visual Studio Code、Slack 等应用均由 Electron 驱动。
  • 后端开发:Node.js 的出现将 JavaScript 从浏览器带到了服务器端。如今,JavaScript 在后端开发中也扮演重要角色,尤其是在构建高并发、高实时性应用(如 WebSocket 和 API 服务)时,Node.js 的非阻塞异步模型表现出色。
  • 嵌入式与物联网:在物联网领域,像 Johnny-Five 这样的框架使开发者可以用 JavaScript 来控制硬件设备,这在以前是由 C 或 C++ 主导的领域。JavaScript 已经逐步进入更多的硬件开发领域。

JavaScript 的痛点

流行和普及

近年来,随着前端领域的快速发展,JavaScript 得到了广泛的应用与喜爱。借助 JavaScript 的跨平台性、丰富的生态系统以及开发效率,越来越多的开发者使用 JavaScript 构建从简单网页到复杂应用的各种项目。

然而,JavaScript 的流行并不意味着它没有缺点。由于历史因素和最初的设计理念,JavaScript 存在不少痛点,这也是开发者们经常讨论的话题。

早期的作用域问题

在 ES5 及之前版本,JavaScript 采用 var 关键字定义变量。var 存在函数作用域的问题,而非块级作用域,这与许多开发者对变量生命周期的理解不一致,容易导致变量的作用范围不清晰,从而引发一些难以排查的 bug。

案例:在 for 循环中使用 var,循环结束后 var 变量依然存在于外部作用域中,这种特性常常让开发者感到困惑。

javascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
// 输出:3, 3, 3

这一问题后来被 ES6 引入的 let 和 const 关键字解决,它们遵循块级作用域的规则,让变量的作用范围更加明确和安全。

数组的实现与内存管理

JavaScript 的数组最初设计并不是像其他编程语言那样使用连续的内存块。JavaScript 数组实际上是动态数组类似于对象的结构,数组的每个元素并不是连续存储的。这种设计在某些情况下影响了性能,尤其是处理大量数据时。

  • 案例:在处理数据密集型应用时,JavaScript 数组的操作性能远不如像 C 或 C++ 中那种基于连续内存的数组。
  • 尽管 JavaScript 的引擎在不断优化数组操作的性能,但仍然在一些高性能要求的场景中表现不如其他语言。

缺乏类型系统

JavaScript 是一种动态类型语言,缺乏静态类型检查机制。这虽然提供了极大的灵活性,但也让开发者在大型项目中面对更大的维护挑战。

  • 案例:在开发大型项目时,由于 JavaScript 不做类型检查,错误的参数类型传递到函数中不会引发编译错误,可能在运行时导致难以调试的 bug。
  • 影响:大型团队或项目中,动态类型可能增加了代码维护的难度,导致开发者需要依赖更多的单元测试来确保代码的正确性。
  • 改进:虽然 JavaScript 本身没有原生的类型系统,但 TypeScript 的出现为 JavaScript 社区提供了静态类型检查的解决方案。如今,TypeScript 在大型项目中已广泛应用,弥补了 JavaScript 的类型检测不足。

JavaScript的不断演进

JavaScript 虽然有历史上的不足,但它也在不断进化和完善。自从 ES6 发布以来,JavaScript 社区每年都会推出新的标准(如 ES7、ES8),带来了许多强大的语言特性,逐步解决了早期的问题,并提升了开发体验和性能。

ES6 带来的改进

块级作用域:let 和 const 的引入解决了 var 作用域不明的问题,提升了代码的可维护性。

箭头函数:箭头函数简化了函数表达式,并且保持了 this 的绑定问题,极大简化了回调函数中的上下文处理。

Promise 和 async/await:在异步编程中,传统的回调地狱问题使代码变得难以维护,而 ES6 引入的 Promise 和后续的 async/await 提升了异步代码的可读性,使得代码更具结构化。

ES7、ES8 带来的现代特性

异步迭代器:ES8 引入了异步迭代器,使开发者可以更简洁地处理流式异步操作。

对象的扩展操作符:JavaScript 中的对象处理变得更加便捷,使用扩展操作符可以轻松地复制和合并对象。

类型系统的补足:TypeScript 的崛起

虽然 JavaScript 本身没有原生的静态类型系统,但 TypeScript 的出现弥补了这一不足。TypeScript 在保留 JavaScript 动态特性的同时,引入了类型检查,使得大型项目中的代码更加健壮和可维护。现在,很多现代项目如 Angular 都已经内建对 TypeScript 的支持。

WebAssembly:高性能计算的补充

JavaScript 虽然在很多领域表现优秀,但在某些高性能场景中依然力不从心。WebAssembly 的出现,提供了与 JavaScript 互补的能力,开发者可以使用 C、C++、Rust 等语言编写高效代码,并通过 WebAssembly 直接在浏览器中执行。这使得 JavaScript 在性能关键的场景下也能与其他语言协作,而不是完全依赖 JavaScript 本身。

缺少类型校验带来的问题

错误发现的最佳时机

在软件开发中,存在一个广泛的共识:错误出现得越早,修复成本越低,项目的稳定性越高。因此,程序员追求在开发周期的早期阶段发现错误,并尽可能避免将错误留到运行时才暴露。

  • 写代码时发现错误比编译时发现错误更好:现代 IDE (集成开发环境)如 Visual Studio Code 或 IntelliJ IDEA 等,提供实时的代码分析和错误提示功能,帮助开发者在编写代码时就发现可能的问题。
  • 编译期间发现错误比运行时发现错误更好:编译期的类型检测,尤其在静态类型语言中,可以在代码运行之前,帮助捕捉潜在的错误。此时修复的成本比在运行期间小很多。
  • 开发阶段发现错误比测试阶段和上线后更好:在代码编写阶段就解决问题,可以减少调试和测试的时间成本。

动态类型的潜在风险

JavaScript 是一种动态类型语言,这意味着类型检查发生在代码的运行期间,而不是编写或编译期间。这带来了灵活性,也带来了潜在的错误风险。看下面这段 JavaScript 代码:

javascript
function getLength(str) {
  return str.length
}

getLength('ab') // 正确调用
getLength() // 运行时报错,但编写时不会报错
  • 问题分析
    在 getLength() 调用中,str 参数未传递,JavaScript 默认将其值设为 undefined。由于 undefined 类型没有 length 属性,运行时会抛出错误:

    javascript
    TypeError: Cannot read property 'length' of undefined

    这种错误只能在运行时才被捕获,开发者在编写代码和编译期间不会收到任何警告或错误提示。

  • JavaScript 的挑战
    由于缺乏编译时的类型检查,开发者只能依赖运行时错误报告,或者通过大量的单元测试来尽早捕捉这些潜在问题。这在大型项目或团队合作中会引发很多维护问题,因为每个函数或对象的期望类型无法明确规定,导致代码健壮性降低。

静态类型的能力

TypeScript 是 JavaScript 的一个超集,它为 JavaScript 增加了静态类型系统。这种类型系统在编写代码时就能进行静态分析,帮助开发者捕捉到潜在的类型错误。在上面的例子中,我们可以通过 TypeScript 来避免上述问题。

typescript
function getLength(args: { length: number }): number {
  return args.length
}

getLength('abc')      // 正确调用,字符串有 length 属性
getLength([1, 2, 3])  // 正确调用,数组有 length 属性
getLength({})         // 错误调用,IDE 会提示:类型 "{}" 不符合 "{length: number}",缺少 length 属性
getLength(123)        // 错误调用,IDE 会提示:类型 "number" 不符合 "{length: number}" 类型

// 这里为什么加入export
// 是因为ts默认每一个文件都是一个独立的模块,如果不加上模块的话,就会当成全局
// 加上export之后,就是当成一个独立的模块,就不会和全局的getLength发生冲突了
export {}
  • 类型检查的好处
    TypeScript 在代码编写时就能对参数类型进行检测,如果传递了不符合类型要求的值,IDE 会立即发出警告。这避免了动态类型语言在运行时才发现错误的情况,降低了潜在的 bug 风险。
  • 补充说明
    TypeScript 还引入了导出(export)和模块化的概念。默认情况下,每个文件被视为一个独立的模块,因此要使用 export {} 来确保文件不会与全局作用域中的其他代码发生冲突。
    typescript
    export {}  // 保证文件被视为独立模块,防止全局变量冲突

避免类型错误的最佳实践

JavaScript 中的风险

在 JavaScript 中,类型错误是一类非常常见的错误,主要原因在于 JavaScript 是一门动态类型语言,对函数参数并没有强制的类型限制。导致很多错误只能在运行时暴露,而无法在开发或编译阶段提前捕获。

  • 动态类型的隐患: JavaScript 不会在编写或编译时检查我们传入的参数类型,直到代码实际运行时才发现潜在错误。例如:
    javascript
    function getLength(str) {
      return str.length
    }
    
    getLength('hello')  // 正常工作,输出: 5
    getLength()         // 运行时错误: TypeError: Cannot read property 'length' of undefined
    当调用 getLength() 时,未传递参数导致 str 为 undefined,从而抛出运行时错误。这种错误无法在编写代码时被检测到,必须等到运行时才能发现。
  • 运行时错误的后果: 这种类型错误可能导致后续代码无法执行,甚至让整个应用崩溃。这在大型项目或复杂应用中,可能造成无法预料的后果。

你能避免这些风险?

你可能会认为,在简单的 demo 中,这样的错误很容易被避免,而且即使出现错误,也很容易在调试时解决。然而,问题的复杂性在于:

  • 复杂项目中的错误管理: 在大型项目中,代码复杂度、依赖的第三方库增加,开发者很难全面跟踪每个函数所需的参数类型和限制。你无法确保每个函数调用都传递了正确的参数,尤其是在调用第三方库时,文档不完整或开发者未明确传递的参数类型要求,导致错误更难追踪。

  • 维护代码的难度: 当项目规模扩大或团队合作时,每个开发人员的代码习惯不同,代码逻辑也会变得更加复杂。没有明确的类型约束容易造成误传参数的情况,这种错误往往难以调试和修复,特别是在运行时才暴露出来。

在编译期发现错误

TypeScript 是 JavaScript 的静态类型超集,它为 JavaScript 添加了类型检查机制,从而在编写代码时就能捕获类型错误。通过使用 TypeScript,开发者能够在编译期间就检测出潜在的错误,而不是等到运行时才发现。

  • 强制类型检查: TypeScript 允许为函数参数和返回值指定类型,从而在编译期就可以进行严格的类型检查,避免不必要的错误。例如:
    typescript
    function getLength(str: string): number {
      return str.length
    }
    
    getLength('hello')  // 正常工作,返回: 5
    getLength()         // 编译错误:Expected 1 arguments, but got 0.
    在 TypeScript 中,传递空参数会直接导致编译错误,这样你就可以在开发阶段发现并修复错误,而不是让错误留到运行时。
  • 类型限制的好处: 通过为参数指定类型,TypeScript 确保函数调用时,必须传递符合期望类型的参数。例如:
    typescript
    function getLength(str: string): number {
      return str.length
    }
    
    getLength(123)  // 编译错误:Argument of type 'number' is not assignable to parameter of type 'string'.
    这种类型系统的严格检查,可以帮助开发者在编译期发现潜在的类型错误,从而避免这些错误在运行时暴露。

编译期错误与运行时错误的比较

通过使用 TypeScript 的静态类型系统,很多问题可以在代码编译期间就被检测出来,而不是等到运行时才发现。这对于大型项目的开发尤为重要。

  • 编译期错误的优势
    • 开发期间发现问题:在编写代码时,IDE 可以根据 TypeScript 的类型检查功能,立即提示你参数类型的错误,这比运行时发现错误要方便得多。
    • 提高代码的自解释性:明确的类型声明使代码更加清晰,并且团队成员可以很容易理解函数的期望输入和输出类型。
    • 减少调试时间:通过在编译期发现类型错误,可以减少由于类型错误导致的调试时间和运行时崩溃。
  • 运行时错误的缺点
    • 难以追踪:运行时错误往往是在程序执行到某一特定条件下才会发生,可能很难通过单元测试或简单的代码分析来捕捉到这些错误。
    • 生产环境风险:如果错误没有被及早发现,可能会在上线后才暴露,导致生产环境的崩溃或不稳定。

二. 邂逅 TypeScript 开发

在现代前端开发中,TypeScript(以下简称 TS)已成为提升代码质量和开发效率的重要工具。 本文将深入探讨如何在 JavaScript(JS)中添加类型约束,全面认识 TS,分析其特点,并了解众多项目为何选择采用 TS。为了满足不同读者的需求,内容将根据用户和开发者进行分层讲解,确保信息的全面性和条理性。

JS 添加类型约束

用户视角
对于前端开发者来说,JavaScript 的动态类型特性虽然带来了灵活性,但在大型项目中,缺乏类型约束可能导致代码难以维护和易出错。为了弥补这一缺陷,许多公司和开源社区推出了自己的类型检查方案:

  • 2014年,Facebook 推出了 Flow
    • 目的:为 JavaScript 提供静态类型检查,提升代码的可靠性。
    • 应用:主要用于大型项目,特别是 React 应用。
  • 同年,Microsoft 推出了 TypeScript 1.0
    • 目的:通过扩展 JavaScript,提供更强大的类型系统,支持面向对象编程,提升开发者的生产力。
    • 应用:广泛应用于各种规模的项目,特别是在需要严格类型检查的企业级应用中。

开发者视角
从开发者的角度来看,添加类型约束不仅提高了代码的可读性和可维护性,还能在编译阶段捕捉潜在的错误,减少运行时异常。以下是两种类型检查工具的详细对比:

Flow

  • 类型推断:Flow 能够自动推断变量类型,减少类型注解的负担。
  • 集成:与 React 等框架深度集成,适用于前端开发。
  • 社区支持:相对较小,但在特定社区中有一定的用户基础。
  • 使用场景:适用于已经使用 React 并希望快速集成类型检查的小到中型项目。

TypeScript

  • 全面的类型系统:支持接口、枚举、泛型等复杂类型,适用于大型项目。
  • 工具链支持:与各种开发工具和编辑器(如 VSCode)高度集成,提供智能提示和自动补全。
  • 社区和生态系统:拥有庞大的社区支持和丰富的第三方库,持续更新和维护。
  • 使用场景:适用于需要严格类型检查、面向对象编程以及大型企业级项目的开发。

认识 TypeScript

用户视角
虽然了解了 TS 可以为 JS 添加类型约束,但更全面地认识 TS 有助于更好地利用其优势。以下是 TS 的基本定义和特点:

  • GitHub 定义:TypeScript is a superset of JavaScript that compiles to clean JavaScript output
  • TypeScript 官网:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript
  • 翻译:TS 是拥有类型的 JS 超集,可以编译成普通、干净、完整的 JS 代码。

开发者视角
深入理解 TS 的特性和工作原理,有助于更高效地在项目中应用 TS。以下是对上述定义的详细解读:

  • 超集特性
    • TS 包含所有 JS 的特性,开发者可以逐步将现有 JS 项目迁移到 TS。
    • 支持最新的 ECMAScript 标准(如 ES6、ES7、ES8 等),并在此基础上提供类型系统和语法扩展。
  • 类型系统
    • 静态类型检查:在编译阶段检测类型错误,避免运行时错误。
    • 类型推断:减少显式类型注解,通过上下文自动推断变量类型。
    • 接口与类型别名:定义复杂的数据结构和接口,提高代码的可维护性和可读性。
  • 编译过程
    • TS 代码在编译时转化为 JS,兼容任何支持 ECMAScript 3 或更高版本的 JS 引擎。
    • 不需要依赖于 Babel 等工具,可以独立完成编译过程。

TypeScript 的特点

用户视角
了解 TS 的主要特点,有助于用户理解其在开发过程中的优势:

  • 始于 JS,归于 JS
    • 与现有 JS 代码无缝集成,支持调用 TS 代码和被 TS 代码调用。
    • 编译出的 JS 代码简洁且高效,适用于任何环境。
  • 强大的工具支持
    • 提供静态检查和代码重构功能,提升开发效率。
    • 类型定义文件(.d.ts)支持第三方库的类型声明,增强代码提示和自动补全。
  • 先进的 JS 特性
    • 支持最新的 ECMAScript 特性,如异步函数、装饰器(Decorators)等。
    • 提供额外的语法扩展,如枚举(Enum)、元组(Tuple)等,丰富了语言的表达能力。

开发者视角
从技术角度深入分析 TS 的特点,有助于开发者更好地利用其功能:

  • 类型系统的灵活性
    • 可选类型:类型注解是可选的,开发者可以根据需要逐步引入。
    • 联合类型与交叉类型:支持复杂类型的组合,提高代码的灵活性。
    • 泛型:支持泛型编程,增强代码的复用性和类型安全性。
  • 面向对象编程支持
    • 提供类(Class)、接口(Interface)、抽象类(Abstract Class)等面向对象编程特性,适用于大型项目的架构设计。
  • 模块化支持
    • 支持 ES6 模块和 CommonJS 模块,方便项目的模块化管理和依赖注入。
  • 编译选项
    • 丰富的编译选项(如 strict 模式、目标 ES 版本等),开发者可以根据项目需求进行配置,优化编译过程和输出结果。

众多项目采用 TypeScript

用户视角
TypeScript 的广泛应用证明了其在前端开发中的重要性和实用性。以下是一些采用 TS 的知名项目和框架:

  • Angular
    • 从早期版本开始,Angular 使用 TS 进行开发和重构。
    • 开发者需要掌握 TS 才能高效开发和维护 Angular 应用。
  • Vue.js
    • Vue 2.x:采用 Flow 进行类型检查。
    • Vue 3.x:全面转向 TS,约 98.3% 的代码使用 TS 重构,提升了框架的可维护性和类型安全性。
  • VSCode
    • 作为最流行的代码编辑器,VSCode 本身就是使用 TS 开发的,充分利用了 TS 的类型系统和开发工具支持。
  • Ant Design
    • 在 React 生态中广泛使用的 ant-design UI 库,大量使用 TS 编写,提供了详细的类型定义,提升了开发体验。
  • 小程序开发
    • 许多小程序框架和工具链(如 Taro)支持 TS,提升了小程序代码的可维护性和开发效率。

开发者视角
从技术实施的角度,分析为何众多项目选择 TS,有助于开发者理解其背后的逻辑和优势:

  • 代码质量和维护性
    • 静态类型检查和类型系统提升了代码的可靠性,减少了运行时错误。
    • 强类型系统使得代码更易于理解和维护,特别是在大型团队和项目中。
  • 开发效率
    • 智能提示和自动补全功能减少了开发时间,提高了编码效率。
    • 类型定义文件支持第三方库的类型声明,减少了类型错误的可能性。
  • 生态系统和社区支持
    • TS 拥有庞大的社区和丰富的生态系统,提供了大量的类型定义文件和第三方工具,方便开发者快速上手和集成。
  • 未来发展
    • TS 紧随 ECMAScript 标准的步伐,持续引入新特性,保证其在未来开发中的适用性和先进性。

前端学不动系列

在前端技术迅速发展的今天,开发者面临着不断学习新技术的压力。以 Deno 为例,其在 issue 中出现了一个经典问题:

image-20221012205646127

用户视角
对于许多开发者来说,快速变化的前端技术栈带来了巨大的学习压力。新技术层出不穷,从框架到工具,每一次更新都需要投入大量时间和精力去学习和适应。这种持续的学习压力可能导致一些开发者感到疲惫,甚至产生“学不动”的情绪。

开发者视角
面对不断涌现的新技术,开发者需要制定合理的学习计划和策略,以高效地掌握必要的技能,同时避免因技术过载而影响工作效率。以下是一些应对策略:

  • 选择性学习:根据项目需求和职业规划,优先学习那些对当前工作最有帮助的技术。
  • 持续学习:保持学习的连续性,而不是一蹴而就,逐步掌握新技术。
  • 实践应用:通过实际项目应用新学的技术,加深理解和记忆。
  • 社区参与:参与技术社区和讨论,获取最新的技术动态和学习资源。

大前端的发展趋势

用户视角
“大前端”指的是涵盖 Web 前端、移动端(如 React Native、Flutter)、桌面端(如 Electron)等多个平台的开发者。这一领域的发展趋势展示了前端技术的多样性和广泛应用:

  • 多平台开发
    • 客户端开发者:从 Android 到 iOS,或者从 iOS 到 Android,再到 React Native、Flutter,开发者需要掌握多种平台的开发技术。
    • 前端开发者:从 jQuery 到 AngularJS,再到主流框架 Vue、React、Angular,以及小程序开发,技术栈不断丰富。
  • 技术更新快速
    • 语言和框架:不仅需要学习 ES 的新特性,还要掌握 TS,以及新框架如 Vue 3.x、React 18 等。
    • 工具链:工具如 Vite 等不断更新,开发者需要跟上工具的演进。
  • 解决痛点
    • 每一种新技术的出现,通常都是为了解决之前技术的某些痛点。例如,TS 解决了 JS 的类型安全问题,Vite 提升了构建速度和开发体验。

开发者视角
从开发者的角度来看,大前端的发展趋势反映了技术生态的不断演进和优化:

  • 模块化与组件化
    • 越来越多的框架和工具支持模块化和组件化开发,提升代码复用性和维护性。
  • 性能优化
    • 新技术和工具不断涌现,旨在提升应用的性能和响应速度,如 Vite 提供了极速的开发启动和热更新体验。
  • 类型安全
    • TS 的广泛应用反映了对类型安全的重视,帮助开发者提前发现和解决潜在问题,提升代码质量。
  • 生态整合
    • 各种技术工具和框架之间的整合越来越紧密,形成完善的生态系统,支持开发者从前端到全栈的开发需求。
  • 开发者体验
    • 工具链的改进和新特性的引入,极大地提升了开发者的体验和效率,减少了重复劳动和错误。

实际案例分析

通过实际案例,深入理解 TS 的应用和优势。

案例1:从 Flow 迁移到 TypeScript

情境描述
某大型前端项目最初采用 Facebook 的 Flow 进行类型检查,但随着项目规模的扩大和团队的需求变化,决定迁移到 TypeScript。

迁移步骤

  1. 评估现有代码
    • 使用 Flow 提供的工具分析现有类型注解,识别需要迁移的部分。
    • 确定哪些类型注解需要手动转换,哪些可以自动转换。
  2. 配置 TypeScript 环境
    • 安装 TypeScript:
    bash
    npm install --save-dev typescript
    • 初始化 tsconfig.json:
    bash
    npx tsc --init
    • 配置编译选项,根据项目需求调整 tsconfig.json 文件。
  3. 转换类型注解
    • 将 Flow 的类型注解转换为 TS 的类型语法。
    • 处理 Flow 特有的类型特性,替换为 TS 等效的类型。
    • 使用工具如 flow-to-ts 进行自动转换。
  4. 解决类型错误
    • 编译项目,逐步解决 TS 报告的类型错误,确保类型安全。
    • 使用 TS 的类型检查功能,发现并修复潜在的代码问题。
  5. 更新构建工具
    • 配置 Webpack 或其他构建工具,集成 TypeScript 编译步骤。
    • 确保构建流程顺利,将 TS 编译为 JS。
  6. 测试和验证
    • 运行现有测试,确保迁移过程中功能未受影响。
    • 编写新的测试用例,覆盖迁移后的代码,确保代码质量。

迁移结果
迁移完成后,项目享受到了 TS 带来的更强大的类型系统和更丰富的开发工具支持,开发效率和代码质量显著提升。

案例2:大型项目引入 TypeScript

情境描述:一家公司拥有一个庞大的前端项目,团队规模不断扩大,代码维护变得愈加困难。决定引入 TypeScript 以提升代码质量和开发效率。

引入步骤

  1. 团队培训
    • 对团队成员进行 TypeScript 培训,确保每个人都了解其基本概念和使用方法。
    • 提供在线课程和内部分享会,提升团队整体水平。
  2. 逐步迁移
    • 采用渐进式迁移策略,先将部分模块转换为 TS,逐步覆盖整个项目。
    • 使用 allowJs 选项,允许在 TS 项目中使用 JS 文件,便于逐步迁移。
  3. 配置 TypeScript
    • 设置严格的类型检查选项,确保代码质量。
    • 集成 ESLint 和 Prettier,统一代码风格和格式。
    • 配置 tsconfig.json,启用 strict 模式,提高类型检查的严格性。
  4. 重构代码
    • 利用 TS 的类型系统重构现有代码,提升代码的可读性和可维护性。
    • 定义接口和类型别名,优化模块之间的接口设计。
  5. 自动化测试
    • 增加类型测试,确保代码在类型层面无误。
    • 集成 TS 类型检查到 CI 流水线,确保每次提交都符合类型安全要求。

引入结果
项目的代码质量和开发效率显著提升,类型系统帮助团队在开发过程中提前发现潜在问题,减少了后期的维护成本。团队成员对 TS 的掌握程度提高,整体开发流程更加高效和可靠。

技术要点与最佳实践

技术要点

  1. 类型定义文件(.d.ts)
    • 为第三方库编写或引用现有的类型定义文件,确保类型安全。
    • 使用 DefinitelyTyped 提供的类型定义库。
    • 示例:为一个没有类型定义的库添加自定义类型定义。
    typescript
    // types/custom-library/index.d.ts
    declare module 'custom-library' {
      export function customFunction(param: string): number;
    }
  2. 严格模式
    • 启用 tsconfig.json 中的严格模式("strict": true),提高类型检查的严格性,避免潜在错误。
    • 示例:
    bash
    {
      "compilerOptions": {
        "strict": true
      }
    }
  3. 类型推断与显式注解
    • 利用 TS 的类型推断能力,减少显式类型注解的冗余。
    • 在必要时添加显式类型注解,增强代码的可读性和可维护性。
    • 示例:
    typescript
    // 类型推断
    let message = "Hello, TypeScript"; // 自动推断为 string
    
    // 显式注解
    let count: number = 10;
  4. 模块化编程
    • 采用模块化编程,利用 import 和 export 管理代码依赖,提升代码组织性。
    • 示例:
    typescript
    // math.ts
    export function add(a: number, b: number): number {
      return a + b;
    }
    
    // app.ts
    import { add } from './math';
    console.log(add(2, 3));
  5. 接口与类型别名
    • 使用接口(interface)和类型别名(type)定义复杂的数据结构,提升代码的可读性和可维护性。
    • 示例:
    typescript
    interface User {
      id: number;
      name: string;
      email: string;
    }
    
    type Point = [number, number];

最佳实践

  1. 渐进式迁移
    • 对现有 JS 项目,采用渐进式迁移策略,逐步引入 TS,减少迁移过程中的风险。
    • 使用 allowJs 选项,允许混合 TS 和 JS 文件,共同构建项目。
  2. 自动化工具集成
    • 集成 TS 编译器与构建工具(如 Webpack),实现自动化编译和打包。
    • 示例:在 Webpack 中配置 ts-loader。
    javascript
    // webpack.config.js
    module.exports = {
      module: {
        rules: [
          {
            test: /\.ts$/,
            use: 'ts-loader',
            exclude: /node_modules/,
          },
        ],
      },
      resolve: { 
        extensions: ['.ts', '.js'],
      },
    }
  3. 持续集成(CI)
    • 在 CI 流水线中集成 TS 类型检查,确保每次提交都符合类型安全要求。
    • 示例:在 GitHub Actions 中配置 tsc。
    yaml
    # .github/workflows/ci.yml
    name: CI
     
    on: [push, pull_request]
     
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Install dependencies
            run: npm install
          - name: TypeScript type checking
            run: npx tsc --noEmit
  4. 代码审查
    • 在代码审查过程中关注类型定义和使用,确保类型系统的正确应用。
    • 确保每个新功能和修复都符合类型安全要求,避免类型错误。
  5. 文档与注释
    • 为复杂的类型定义和接口添加详细的文档和注释,提升代码的可读性和团队协作效率。
    • 使用 JSDoc 或 TS 的类型注释,提供清晰的代码说明。

三. TypeScript 运行环境

TypeScript 的编译环境

在使用 TypeScript(TS)之前,了解它是如何工作的非常重要。 我们已经了解了 TS 是一种静态类型的超集,它需要通过编译器将 TS 代码转换成浏览器和 Node.js 环境可以执行的 JS 代码。 image-20221012210126325

因此,我们首先需要准备相应的环境来完成这个转换过程。

如何安装 TypeScript

  1. 全局环境中安装 TypeScript: 首先,我们需要在电脑上安装 TypeScript。打开命令行(终端),然后输入下面的命令:
    bash
    npm install -g typescript
    • npm 是 Node.js 的包管理工具,用于安装各种 JavaScript 库和工具。
    • -g 表示全局安装,这意味着你可以在任何地方使用 TypeScript 的命令。
  2. 确认安装成功: 安装完成后,你可以输入以下命令来检查 TypeScript 是否安装成功,并查看版本号:
    bash
    tsc --version
    • tsc 是 TypeScript 的编译器命令,它用于将 TypeScript 代码转换为 JavaScript 代码。
    • 通过 npm install -g typescript 命令安装 TypeScript 时,npm 会将 tsc 命令的可执行文件放置在你的系统 PATH 中。这样,无论你在命令行的哪个位置,都可以直接调用 tsc 命令。

tsc 命令怎么来的

当你全局安装 TypeScript 时,npm 会执行以下几个步骤:

  • 下载 TypeScript 包:npm 会从其注册表中下载 TypeScript 的最新版本,并将其解压到你的全局 node_modules 目录中。
    全局 node_modules 目录所在位置

    全局 node_modules 目录的位置取决于你的操作系统和 Node.js 的安装方式。以下是常见的默认路径:

    • Linux / macOS: /usr/local/lib/node_modules
    • Windows: C:\Program Files\nodejs\node_modules

    你可以使用以下命令查看全局模块的确切安装位置:

    bash
    npm root -g

    这个命令会返回全局 node_modules 目录的路径。

  • 创建可执行文件:npm 会将 TypeScript 包中的一些可执行文件复制到你的全局 node_modules 目录中。 包内有一个名为 tsc 的可执行文件。这个文件是 TS 编译器的入口点,负责将 TS 代码编译为 JS 代码,
  • 更新系统 PATH:npm 在安装时会将可执行文件的路径添加到你的系统 PATH 中,当在命令行中执行 tsc 命令时,这样操作系统就可以在系统 PATH 中找到并执行这个命令。

通过这些步骤,你就能够在命令行中直接使用 tsc 命令了。这是因为操作系统能够找到这个命令并将其执行。

TypeScript 的运行环境

手动编译和运行

每次查看 TypeScript 代码的结果,如果都要经过两个步骤会比较麻烦:

  1. 编译 TypeScript 文件: 你需要先把 .ts 文件转换成 .js 文件,命令是:

    bash
    tsc 文件名.ts

    例如:

    bash
    tsc example.ts

    这个命令会生成一个同名的 .js 文件(如 example.js),这就是可以在浏览器或 Node.js 中运行的 JS 代码。

  2. 运行 JavaScript 文件: 然后,你需要在浏览器或 Node.js 环境中运行这个生成的 JS 文件。

直接运行 TS 代码

有没有更简单的方法? 当然可以!我们可以使用以下两种方法来简化这个过程:

  1. 使用 Webpack 运行 TypeScript: 通过 Webpack 配置一个本地的 TS 编译环境和开启一个本地服务,你可以直接在浏览器中运行 TS 代码。

    • 安装 Webpack 和相关依赖: 首先,你需要在项目目录中安装 Webpack 和一些必要的插件。运行以下命令:

      bash
      npm i --save-dev webpack webpack-cli ts-loader typescript
      • webpack 是一个模块打包工具,可以将你的 TS 代码编译成 JS。
      • ts-loader 是一个 Webpack 插件,用于处理 TS 文件。
    • 创建 Webpack 配置文件: 在项目根目录下创建一个名为 webpack.config.js 的文件,内容如下:

      js
      const path = require('path')
      module.exports = {
        entry: './src/index.ts',
        module: {
          rules: [
            { test: /\.ts$/, 
              use: 'ts-loader', 
              exclude: /node_modules/ 
            },
          ],
        },
        resolve: {
          extensions: ['.ts', '.js'] // 支持的文件扩展名
        },
        output: {
          filename: 'bundle.js', // 输出文件名
          path: path.resolve(__dirname, 'dist') // 输出目录
        }
      }
      • 创建项目结构: 创建一个 src 文件夹,并在其中创建一个 index.ts 文件,写入你的 TS 代码。
      • 运行 Webpack: 在命令行中运行以下命令来打包代码:
        bash
        npx webpack

      这将根据配置文件编译 TS 代码并生成 dist/bundle.js 文件。你可以在 HTML 文件中引入这个 JS 文件进行运行。

    • 对应的文章:https://mp.weixin.qq.com/s/wnL1l-ERjTDykWM76l4Ajw

  2. 使用 ts-node 运行 TypeScript: 这个工具可以让你直接运行 TS 代码,而不需要每次手动编译成 JS 代码。

    • 全局安装 ts-node: 在命令行中输入下面的命令:

      bash
      npm i ts-node -g
    • 安装依赖: ts-node 需要一些额外的工具,我们也要安装它们。输入以下命令:

      bash
      npm i tslib @types/node -g
      • tslib 是 TypeScript 的运行时库,提供一些基本功能。
      • @types/node 包含 Node.js 的类型定义,帮助 TypeScript 理解 Node.js 环境。
    • 安装好后,你就可以使用 ts-node 来运行 TypeScript 文件了。只需在命令行中输入:

      bash
      ts-node 文件名.ts
    • 例如:

      bash
      ts-node example.ts

    这样你就可以直接看到 TypeScript 代码的运行效果,省去了每次编译的麻烦。

四. TypeScript 变量声明

变量的类型声明(类型注解)

  • 什么是类型注解?
    在 TypeScript 中,变量需要定义数据类型,称为类型注解(Type Annotation)。这样可以帮助 TypeScript 编译器检查代码中的类型错误。

  • 变量声明格式
    完整的变量声明格式如下:

    typescript
    var/let/const 变量名: 数据类型 =

    其中,变量名是你要定义的标识符,数据类型是这个变量应该存储的类型,值是为这个变量赋予的初始值。

  • 示例:字符串类型声明
    比如我们想声明一个字符串类型的变量 message,可以这样写:

    typescript
    let message: string = '你好'
    • 声明了类型后编译器就会进行类型检测,声明的类型可称为 类型注解(Type Annotation)。
    • 如果我们给 message 赋值其他类型的值,那么 TS 编译器会在编译阶段检测到类型不匹配并提示错误:
      typescript
      let msg: string = 'hello world'
      // 不能将类型“number”分配给类型“string”。
      msg = 10

    注意事项

    string(小写)和 String(大写)的区别:

    • string 是 TypeScript 中的基本字符串类型,用于声明普通字符串变量。
    • String 是 ECMAScript 标准中的字符串对象类型,表示的是 JavaScript 中的 String 类。

    虽然它们看起来很像,但在 TypeScript 中,最好使用小写的 string 来声明字符串变量。

声明变量使用的关键字

  • 变量声明的方式
    在 TypeScript 中,变量声明的方式与 ES6 标准一致,你可以使用以下三种关键字:

    var:较早的声明方式,但不推荐使用。
    let:推荐用于声明会被重新赋值的变量。
    const:用于声明不会被重新赋值的常量。

    typescript
    var msg: string = 'later'
    let num: number = 100
    const num2: number = 200
  • 为什么不推荐使用 var?
    var 声明的变量没有块级作用域,容易导致代码中的逻辑错误。使用 let 和 const 可以避免这些问题。因此,TypeScript 和 ESLint 等工具都建议使用 let 或 const 代替 var。
    例如:

    typescript
    // Forbidden 'var' keyword, use 'let' or 'const' instead(no-var-keyword)tslint
    var myname: string = '你好'
    // 使用 var 会导致 tslint 报错:no-var-keyword 错误:请使用 let 或 const 代替 var。

变量的类型推导(类型推断)

  • 什么是类型推导?

    在开发中,有时候为了方便起见我们并不会在声明每一个变量时都写上对应的数据类型。 我们更希望可以通过 TypeScript 本身的特性帮助我们推断出对应的变量类型。
    当我们声明变量时,如果不显式声明类型,TS 会根据变量的初始值自动推断出它的类型。这个过程叫做类型推导(Type Inference)。

    示例:

    typescript
    let message = '你好' // => let message: string = '你好' 
    // TS 编译器会在编译阶段检就推导出来 'message' 值为 'string' 类型

    虽然我们没有明确指定 message 的类型,但是 TS 已经推断它是 string 类型。接下来,如果我们尝试为 message 赋值一个数字,就会报错:

    typescript
    // Type 'number' is not assignable to type 'string'. ts(2322)
    message = 10
  • 类型推导的原理

    变量第一次赋值时,TypeScript 会根据初始值的类型推断变量的类型。 在上面的例子中,因为 message 的初始值是字符串 '你好',所以 message 被推断为 string 类型。 这意味着,如果没有显式声明类型,TypeScript 依然能够根据赋值来推断类型。

    引用描述

    Keep in mind, we don't always have to write explicit type annotations. in many cases, TypeScript can even just infer(of "figure out")the types for us even if we omit them.
    请记住,我们并不总是必须编写显式的类型注释. 在很多情况下,TypeScript甚至可以为我们推断(或“找出”)类型,即使我们忽略它们。

  • 什么时候使用类型推导?
    当变量的初始值可以明确表明类型时,可以不写类型注解,让 TypeScript 自动推断。
    例如:

    typescript
    let name = 'Alice' // 推导出 name 是 string 类型
    let age = 30       // 推导出 age 是 number 类型
    const height = 1.75 // 推导出 height 是 1.75 的字面量类型
  • let 与 const 的推导区别

    let 推导出来的是通用类型:let 声明的变量会被推导为一个常见的类型(如 string 或 number)。
    const 推导出来的是字面量类型:const 声明的变量会被推导为字面量类型,即具体的值本身。

    例如:

    typescript
    let height = 1.88   // height 被推导为 number 类型
    const height = 1.88 // height 被推导为 1.88 的字面量类型

    字面量类型是指变量的类型就是这个具体的值,例如 1.88,而不是通用的 number 类型。

五. JS 原有数据类型在 TS 中的表示差异

我们经常说 TypeScript 是 JavaScript 的超集,意味着它在 JavaScript 的基础上增加了更多特性。

image-20221012221049503

number 类型(数字类型)

TypeScript 与 JavaScript 一样,不区分整数类型(int)和浮点型(double),统一使用 number 类型表示所有的数字。

typescript
let num: number = 100
num = 20
num = 6.66

ES6 标准中新增了二进制、八进制 、十六进制的表示方法。 而 TS 也是支持多种进制的表示方法:

typescript
num = 100   // 十进制
num = 0b110 // 二进制
num = 0o555 // 八进制
num = 0xf23 // 十六进制

在 TypeScript 中,number 和 Number 虽然看起来相似,但它们实际上是不同的概念,使用时有一定的区别。 以下是它们的具体差别:

  • number 类型

    number 是 TypeScript 中的基本类型(primitive type),它表示的是原始的数值类型,用于定义数值类型的变量。这是我们最常用的表示数字的类型。

    typescript
    const n1: number = 10 // 使用 number 类型表示数字
  • Number 对象类型

    Number 是 JavaScript 中的全局对象(wrapper object),它是 JavaScript 提供的一个构造函数,用于创建 Number 对象类型的实例。这个类型是 number 类型的对象包装器。

    typescript
    const n2: Number = new Number(20)  // 使用 Number 构造函数创建一个 Number 对象
  • number 和 Number 的区别

    特性numberNumber
    类型原始类型(primitive type)对象类型(object type)
    用途用于表示普通的数值(例如 10)用于表示数值的包装对象,包含属性和方法
    实例化直接使用数字即可使用 new 关键字创建实例
    性能原始类型,不需要创建对象,性能较高Number 是对象包装器,使用 new 关键字创建实例,性能较低
  • 使用场景对比

    number 类型更常用,因为它是直接的原始值,性能更优,且是 TypeScript 和 JavaScript 中最常用的数值表示方式。

    typescript
    const n1: number = 10
    console.log(n1.toFixed(2)) // 可以调用基础的方法

    Number 一般不推荐用于普通数值运算,除非你需要用到 Number 对象的特殊方法和属性,例如 Number.prototype 上的一些方法。

    typescript
    const n2: Number = new Number(20)
    console.log(n2.toFixed(2))  // 使用 Number 对象的 toFixed 方法
    console.log(typeof n2)      // "object"
  • 性能与行为

    因为 Number 是对象类型,所以它的操作相对来说要比直接使用 number 慢一些。在大多数情况下,建议直接使用 number 类型,因为这是更高效的方式。

    number 类型的行为:

    typescript
    const num1: number = 100
    console.log(typeof num1)  // "number"

    Number 对象类型的行为:

    typescript
    const num2: Number = new Number(100)
    console.log(typeof num2)  // "object"

    可以看到 Number 实例化后是对象,而 number 是原始类型。

boolean 类型(布尔类型)

在 TypeScript 中,boolean 和 Boolean 之间也有类似的区别。boolean 是原始类型(primitive type),而 Boolean 是 JavaScript 中的构造函数,用于生成 Boolean 对象。以下是它们的具体区别:

  • boolean 类型

    boolean 是 TypeScript 中的基本类型,用于表示布尔值(true 或 false)。

    typescript
    const flag: boolean = true // 直接使用布尔值 true 或 false
  • Boolean 对象类型

    Boolean 是 JavaScript 中的构造函数,用于创建一个布尔对象,包装 true 或 false 值。这种对象类型相对较少使用,因为包装对象会增加内存开销。

    typescript
    const flagObj: Boolean = new Boolean(false) // 使用 Boolean 构造函数创建一个布尔对象
  • boolean 和 Boolean 的区别
    特性booleanBoolean
    类型原始类型(primitive type)对象类型(object type)
    用途用于表示布尔值(true 或 false)用于表示布尔值的包装对象,包含属性和方法
    实例化直接使用布尔值即可使用 new 关键字创建实例
    性能原始类型,不需要创建对象,性能较高Boolean 是对象包装器,使用 new 关键字创建实例,性能较低
    转换行为boolean 值直接为 true 或 falseBoolean 对象总是为 true,除非显式转换为 false
  • 使用场景对比

    通常在代码中直接使用 boolean,Boolean 仅在需要包装对象(如某些复杂的对象方法或属性)时才会使用。

    • boolean 直接表示布尔值,用法简单直接:
      typescript
      const isActive: boolean = true
      console.log(isActive)  // 输出: true
    • Boolean 是对象类型,一般不推荐用于表示简单的布尔值:
      typescript
      const isActiveObj: Boolean = new Boolean(false)
      console.log(isActiveObj)                // 输出: [Boolean: false]
      console.log(typeof isActiveObj)         // 输出: "object"
      console.log(isActiveObj ? "Yes" : "No") // 输出: "Yes",因为对象始终为 `true`

    ⚠️ 注意

    当使用 Boolean 包装对象时,即使对象内容为 false,在条件判断中对象本身依然被视为 true,因此会导致逻辑混淆。推荐使用 boolean 而非 Boolean 来避免这种情况。

string 类型(字符串类型)

在 TypeScript 中,string 和 String 也有类似的区别,string 是原始类型,而 String 是 JavaScript 中的构造函数,用于创建字符串对象。以下是具体的区别:

  • string 类型

    string 是 TypeScript 中的基本类型,用于表示字符串的原始值。它是表示字符串最常用的类型。

    typescript
    const greeting: string = "Hello, world!"
  • String 对象类型

    String 是 JavaScript 的构造函数,用于创建一个字符串对象。使用 new String() 创建的字符串实际上是一个包装对象,而不是一个基本的字符串类型。

    typescript
    const greetingObj: String = new String("Hello, world!")
  • string 和 String 的区别
    特性stringString
    类型原始类型(primitive type)对象类型(object type)
    用途用于表示基本的字符串值用于表示字符串的包装对象,包含属性和方法
    实例化直接使用字符串即可使用 new 关键字创建实例
    性能原始类型,不需要创建对象,性能较高String 是对象包装器,使用 new 关键字创建实例,性能较低
    转换行为直接表示字符串String 对象转换为字符串时,会调用对象的 toString() 方法,返回字符串表示
  • 使用场景对比

    通常我们直接使用 string 类型来表示字符串,而 String 仅在需要特殊对象属性和方法时使用(但这在实际编码中不常见)。

    • string 更常用:
      typescript
      const name: string = 'Alice'
      console.log(name.length) // 输出:5
    • String 是对象,极少用到:
      typescript
      const nameObj: String = new String("Alice")
      console.log(nameObj)          // 输出: [String: 'Alice']
      console.log(typeof nameObj)   // 输出: "object"

Array 类型

TypeScript 支持两种声明数组的方式:

  • string[]:数组中的元素只能是字符串类型。
  • Array<string>: 泛型方式,同样限制数组元素为字符串类型
typescript
// string[]: 数组类型,并且数组中存放的只能是字符串类型
const names: string[] = ['aaa', 'bbb', 'ccc']

// Array<string>: 数组类型,并且数组中存放的只能是字符串类型
const names2: Array<string> = ['aaa', 'bbb', 'ccc']

注意:实际开发中,数组通常只存放相同类型的数据。

typescript
// 混合类型会报错:
names.push(123) // number 类型的参数不能赋值给 string 类型的参数
names2.push(123) // number 类型的参数不能赋值给 string 类型的参数

object(通用对象类型)、{}(空对象类型)、对象字面量类型

在 TypeScript 中,object、{}(空对象类型)和对象字面量类型都有各自的用途和特点。以下是对它们的详细解释和比较:

  • object 通用对象类型

    object 类型在 TypeScript 中表示所有的非原始类型(即非 number、string、boolean、symbol、null 和 undefined ),这个类型可以包含对象、数组、函数等引用类型。

    示例:

    typescript
    const obj: object = { name: 'later' }
    const obj2: object = []       // 数组也属于 object
    const obj3: object = () => {} // 函数也属于 object

    当类型为 Object 时,尽管它能表示对象,但不能访问或修改对象的属性

    示例:

    typescript
    const obj: object = { name: 'later' }
    obj.name// Error: Property 'name' does not exist on type 'object'
    obj['name'] = 'later-zc'// Error: Property name does not exist on type {}
    console.log(obj['name']) // Error: Property name does not exist on type {}
  • {} 空对象类型

    {} 表示一个空对象类型,通常用于约束空对象或不关心属性的类型。
    {} 类型是 TS 中最宽泛的类型之一,允许分配除 null / undefined 外的所有类型。
    但是,用 {} 类型的变量通常不会触发对特定属性的访问错误。

    示例:

    typescript
    const num: {} = 42 // 允许,`{}` 接受所有非 null/undefined 类型
    const obj: {} = { age: 25 }
    obj.age// TS2339: Property age does not exist on type {}
  • 对象字面量类型

    与 {} 和 object 类型不同,对象字面量类型允许我们在定义时指定对象的属性及其类型。
    属性之间可以使用 , 或者 ; 来分割,最后一个属性结尾的分隔符是可选的。
    每个属性的类型部分也是可选的,如果不指定,那么就是 any 类型。
    这是最精确的类型检查方式,用于确保对象具有特定的属性。

    示例:

    typescript
    const obj: { name: string; age: number } = { name: "Bob", age: 30 }
    // 可以访问指定的属性
    console.log(obj.name)  // 输出: "Bob"
    console.log(obj.age)   // 输出: 30
  • 对比总结

    特性object{}对象字面量类型
    类型限制所有非原始类型所有非 null / undefined 类型指定对象具体的属性及类型
    访问属性不能访问具体属性不能访问具体属性可以访问对象字面量类型中指定的属性
    适用场景表示非原始类型的值表示广义上的空对象或宽泛类型指定对象结构,确保对象属性和类型正确
    严格性相对宽泛最宽泛最严格
  • 使用场景示例

    object 类型:适合表示非原始类型的泛型变量,例如只想限制为非原始类型,但无需访问其属性的场景。

    typescript
    function processValue(value: object) {
      // 不能访问属性,但可以传入对象、数组、函数等
    }

    {} 类型:适合非常宽松的类型定义,例如用于类型兼容,但无需特定属性。

    typescript
    const anything: {} = "Hello"  // `{}` 接受几乎所有值

    对象字面量类型:适合要求对象有固定属性的情况,适用于接口设计和明确的结构定义。

    typescript
    const person: { name: string; age: number } = { name: "Alice", age: 28 }
  • 推荐使用

    • 尽量使用 对象字面量类型 来约束对象结构,确保类型安全性和可读性。
    • 仅在需要非常宽松的类型时使用 {} 类型,例如接受所有可能的非 null/undefined 值。
    • 使用 object 表示非原始类型的值,但注意它的限制性。

Symbol 类型

ES6 以前,我们是不可以在对象中添加相同的属性名称的,比如下面的做法:

javascript
const person = {
  identity: 'coder',
  identity: 'teacher'
}

解决方式通常是定义两个不同的属性名:比如 identity1 和 identity2。

现在我们可以通过 Symbol 来定义同名属性,SymbolES6 引入的一种独特且不可重复的值,Symbol 函数返回的是不同的值:

typescript
const s1: symbol = Symbol('title')
const s2: symbol = Symbol('title')

const person = {
  [s1]: 'coder',
  [s2]: 'teacher'
}

虽然 s1 和 s2 的描述相同,但它们是不同的 Symbol 值。

null 和 undefined 类型

在 JavaScript 中,null 和 undefined 是两个基本数据类型。

在 TypeScript 中,它们各自的类型也是 nullundefined

typescript
let n: null = null
let u: undefined = undefined

函数的参数类型

TS 允许我们指定函数的参数类型。

声明函数时,可以在每个参数后添加 类型注解,以声明函数接受的参数类型:

typescript
function greet(name: string) {}
greet('later') // 正确

// 类型 'number' 的参数不能赋给类型 'string' 的参数。ts(2345)
greet(123) 

同时也限制了 参数的数量

typescript
function greet(name: string) {}

// 应有一个参数,但获得 2 个。ts(2554)
greet('小明', '小李') 

函数的返回值类型

TS 也允许我们为函数指定返回值类型,这个类型注解出现在参数列表的后面:

typescript
function sum(num1: number, num2: number): number {
  return num1 + num2
}

大多数情况下,TS 会根据 return 返回值自动推断函数的返回类型,和变量的类型注解一样,无需显式注解。

某些第三方库出于方便理解,会明确指定返回类型,看个人喜好。

匿名函数的参数类型

匿名函数的参数与函数声明的参数会有一些不同:

当一个函数出现在 TS 可以确定该函数会被如何调用的地方时,该函数的参数会自动指定类型

typescript
const names = ['abc', 'cba', 'nba']
names.forEach( item => console.log(item.toUpperCase()) )

我们并没有指定 item 的类型,但是 item 是一个 string 类型:

这是因为 TS 编译器会根据 forEach 函数接收的参数类型以及数组的类型推断出 item 的类型, 我们自己写可能还会写错,所以大多数情况下,匿名函数中的参数不需要添加类型注解, 这种推断称为上下文类型(contextual typing),即根据函数执行的上下文确定参数和返回值的类型。

可选属性类型

对象属性可以通过在属性名后加 ? 来表示为可选的:

typescript
function printCoordinate(point: {x: number, y: number, z?: number}) {
  console.log('x坐标: ', point.x)
  console.log('y坐标:', point.y)
  if (point.z) console.log(point.z)
}

printCoordinate({x: 10, y: 30})

这里 z 是可选的,调用时可以不传递这个属性。

六. TypeScript 新增数据类型

any 类型

在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化。 这个时候我们可以使用 any 类型,any 类型适用于无法确定变量类型的情况(类似于 Dart 语言中的 dynamic 类型), 其特点如下:

可以对 any 类型的变量执行任何操作,包括获取不存在的属性或方法
可以给 any 类型的变量赋值任意类型的值,比如数字、字符串的值。

例如:

typescript
let a: any = 'hello'
a = 123  // 重新赋值为数字
a = true // 重新赋值为布尔值

const aArray: any[] = ['hello', 1, {}]

在以下情况可以使用 any:

  1. 类型复杂且不需要强制约束时(避免频繁类型声明)。
  2. 使用缺少类型声明的第三方库时。

例如,Vue 源码中也会用 any 来做一些类型适配工作。

总结

  • any 表示为任意类型,并且对其进行任意的操作都不会报错。
  • 使用 any 类似于回到 JS 的动态类型中,不推荐随意使用,因为会导致类型检查的缺失。

unknown 类型

unknown 是表示“未知”类型的特殊类型。它和 any 相似,但无法直接对其进行任何操作,需要进行类型校验(类型缩小)。

示例:

typescript
function foo(): string { return 'foo' }
function bar(): number { return 123 }

let res: unknown
const flag = true

if (flag) {
  res = foo()
} else {
  res = bar()
}

// 报错:TS2571: Object is of type unknown 
console.log(res.length)  // 对象 'res' 的类型为 'unknown'

// 必须进行类型校验(类型缩小),才能根据校验后的类型,进行对应的操作
if (typeof res === 'string') { // 类型校验(通过if语句)
  console.log(res.length, res.split(''))
}

总结

unknown 和 any 不同,unknown 在未经类型校验的情况下不允许直接操作。

void 类型

void 表示函数没有返回值,可以省略返回值类型声明,也可以显式声明为 void 类型。

  • 函数没有显式声明返回值类型时,TypeScript 会根据函数体内容自动推导返回类型,并非一律为 void。

    示例:

    typescript
    function sum(num1: number, num2: number) {
      console.log(num1 + num2)
    }

    分析:

    TypeScript 编译器会推导 sum 函数的返回类型为 void,这是因为 sum 函数只执行了 console.log 操作,没有返回任何值。 并不是所有没有显式声明返回类型的函数都被默认为 void 类型,具体情况还要看函数体的内容。

    示例:

    typescript
    function getNumber() {
      return 123
    }

    分析:

    在这个例子中,TypeScript 会推导 getNumber 函数的返回类型为 number,而不是 void,因为函数体中明确地返回了一个 number 值 123。

  • 也可以显式指定返回值类型是 void:

    typescript
    function sum(num1: number, num2: number): void {
      console.log(num1 + num2)
    }
  • 返回值类型为 void 的函数在上下文推导下允许返回 undefined。

    示例:

    typescript
    function foo(): void {
      return undefined
    }

注意

  • 当基于上下文的类型推导(Contextual Typing)推导出返回类型为 void 的时候,并不会强制函数一定不能返回内容。

    示例:

    typescript
    type FnType = () => void
    const foo: FnType = () => 123
    
    const names = ['abc', 'cba', 'nba']
    /*
      Array<string>.forEach(
      	callbackfn: (value: string, index: number, array: string[]) => void, 
      	thisArg?: any
      ): void
    */
    names.forEach(item => item.length)

    这个例子中 foo 函数和 forEach 的回调函数返回值显式声明为 void 类型,但为什么 TS 编译器没有报错?

    在 TypeScript 中,函数声明的返回值类型是 void 时,即时函数返回了值,编译器也不会报错。这种行为主要是 因为 TS 对 void 类型有一定的宽容性设计:在符合 void 类型上下文的情况下,可以返回任意值,编译器会自动 忽略这些返回值,以下是这个行为的原因:

    • void 类型的灵活性:TypeScript 中声明返回类型为 void 时,并不严格要求函数体内不能有返回值, 这意味者在某些场景下,虽然函数返回了一个值,TS 会视而不见,继续视该返回值为 void。这对开发者来说,可以 减少因返回不需要的值而带来的报错,也适用于一些约定 void 返回类型的场景(如事件回调)。
    • forEach 方法的特性:forEach 的回调函数也声明了返回 void 类型,表示回调函数不关注返回值。但在 回调中执行 return 语句返回某个值并不会引发错误,因为 forEach 并不会处理这些返回值,它关注的是回调函数 的副作用。

    示例分析:

    typescript
    type FnType = () => void
    const foo: FnType = () => 123  // 编译器不会报错
    
    const names = ['abc', 'cba', 'nba']
    names.forEach((item) => item.length)  // 回调函数返回了 length,但返回值会被忽略

    在 foo 函数和 forEach 的回调函数中,即便返回了值,TypeScript 编译器也没有报错,因为它们在上下文中都被视为 void 类型,并且不关注实际的返回值。

    那么为什么下面示例会报错:

    typescript
    function foo(): void {
      return 100// TS2322: Type number is not assignable to type void
    }

    这个示例会报错的原因是 直接在 void 类型的函数中返回了一个值 123,与 forEach 或上下文推导的场景不同。在显式声明 void 返回类型的函数中,如果直接使用 return 返回具体的值,TypeScript 编译器会报错,提示不兼容 void 类型。

    区别分析:

    • 函数声明中的 void 类型:当函数声明了 void 返回类型时,它通常表示函数不应该返回值。如果在函数体中使用了 return 并带上具体的值,TypeScript 会认为这是不符合预期的,因此会报错。
    • 上下文推导的 void 类型:在 forEach 回调和一些上下文推导的情况下,虽然返回值被推导为 void,但 TypeScript 会宽容地忽略返回的具体值。例如,在箭头函数中直接返回 123,编译器会把这个值当作不存在。

总结:

  • 函数没有显式声明返回值类型时,TypeScript 会根据函数体内容自动推导返回类型,并非一律为 void。
  • 返回值类型为 void 的函数在上下文推导下允许返回 undefined。(TS 编译器允许这样做而已)
  • 当基于上下文的类型推导(Contextual Typing)推导出返回类型为 void 的时候,并不会强制函数一定不能返回内容。

never 类型

never 表示永远不会产生值的类型,常用于异常处理和类型工具封装。

如果一个函数中是一个死循环或抛出一个异常,那么这个函数会返回东西吗?

不会,那么写 void 类型 或 其他类型作为 返回值类型都不合适,这时就可以使用 never 类型。

示例:

typescript
function loopFun(): never {
  while(true) {
    console.log('Infinite loop')
  }
}

function loopErr(): never {
  throw new Error('An error occurred')
}

never 有什么样的应用场景呢?

这里我们举一个例子,但是它用到了联合类型,后面我们会讲到:

typescript
function handleMessage(message: string | number) {
  switch(typeof message) {
    case 'string':
      console.log(message.length)
      break
    case 'number':
      console.log(message)
      break
      // 假设这是一个公共工具函数
      // 其他人想扩展这个工具函数的时候(扩展message的类型)
      // 对于一些没有处理的case,可以直接报错进行提示
    default: 
      const check: never = message
  }
}

never 的应用场景:

  • 实际开发中很少去定义 never 类型,某些情况下会自动进行类型推导出 never
  • 开发框架(工具)的时候可能会用到 never。
  • 封装一些类型工具的时候,可以使用 never,类型体操的练习:never。

tuple 元组类型

tuple 是元组类型,很多语言中也有这种数据类型,比如 Python、Swift 等

typescript
const tInfo: [string, number, number] = ['later', 18, 1.88]
const item1 = tInfo[0] // 'later',并且知道类型是 string 类型
const item2 = tInfo[1] // 18,并且知道类型是 number 类型

那么 tuple 和 数组 有什么区别呢?

数组中通常存放相同类型的元素。
元组中每个元素类型是确定的,可以通过索引准确获取类型。

示例对比:

typescript
const info: (string | number)[] = ['later', 18, 1.88]
const item1 = info[0] // 不能确定类型

const tInfo: [string, number, number] = ['later', 18, 1.88]
const item2 = tInfo[0] // 一定是 string 类型

元组常用于函数返回值类型:

typescript
// 在函数中使用元组类型是最多的(函数的返回值)
function useState(initialState: number
): [number, (newValue: number) => void] {
  let stateValue = initialState
  function setValue(newValue: number) {
    stateValue = newValue
  }
  return [stateValue, setValue]
}

const [count, setCount] = useState(0)
setCount(100)

总结:

  • 元组类型是 TypeScript 中的特殊类型,它允许你定义一个数组,但数组中的元素类型是已知的,并且是按顺序排列的。
  • 适合需要存储多类型有序数据的情况,如函数返回多个值。

枚举类型

什么是 TS 枚举类型?

枚举(Enum)是 TypeScript 中独特的特性,用于定义一组可能的命名常量。这些常量可以表示一系列相关值,让代码更清晰、易维护,避免使用“魔法数字”或字符串,提供一种更结构化的方式去表达一组固定的值。 枚举可以包含数字或字符串类型的值

示例:

typescript
enum Direction {
  TOP,
  BOTTOM,
  LEFT,
  RIGHT
}

function turnDirection(direction: Direction) {
  switch(direction) {
    case Direction.LEFT:
      console.log('转向左边')
      break
    case Direction.RIGHT:
      console.log('转向右边')
      break
    case Direction.TOP:
      console.log('转向上边')
      break
    case Direction.BOTTOM:
      console.log('转向下边')
      break
    default:
      const myDirection: never = direction
  }
}

const d1: Direction = Direction.LEFT
turnDirection(d1) // 输出: 转向左边

在上面的示例中,Direction 枚举类型中列出了四个方向。我们可以通过 Direction.LEFT、Direction.RIGHT 等来引用这些枚举成员。 turnDirection 函数参数为 Direction 枚举类型,保证了只能传入 TOP、BOTTOM、LEFT 或 RIGHT 之一。

枚举的默认值

在 TypeScript 中,数字枚举的默认值是从 0 开始递增的

示例:

typescript
enum Direction {
  TOP,    // 默认值为0,依次递增
  BOTTOM, // 1
  LEFT,   // 2
  RIGHT   // 3
}

这样定义枚举时,每个成员都对应一个数字值。可以通过枚举成员名称或者数字值来访问它们:

typescript
console.log(Direction.TOP) // 输出: 0
console.log(Direction[0])  // 输出: "TOP"

自定义枚举的初始值

如果希望改变默认值,可以为枚举成员手动赋值。设置一个成员初始值时,后面的成员会从该值递增

示例:

typescript
enum Direction {
  TOP = 100,
  BOTTOM,   // 101
  LEFT,     // 102
  RIGHT     // 103
}

在上面的代码中,TOP 的初始值为 100,后续的成员 BOTTOM、LEFT 和 RIGHT 依次递增。

字符串枚举

TypeScript 还支持字符串枚举类型。使用字符串枚举时,每个成员都需要被赋值为字符串,字符串枚举没有自动递增。 这种方式在某些场景下更适合,比如状态、方向等具有固定名称的值。

示例

typescript
enum Direction {
  TOP = "top",
  BOTTOM = "bottom",
  LEFT = "left",
  RIGHT = "right"
}

⚠️ 注意

在字符串枚举中,每个成员都需要被赋值,不能省略任何一个值,否则 TypeScript 会报错。

示例
typescript
enum Direction {
  TOP = "top",
  BOTTOM = "bottom",
  LEFT = "left",
  RIGHT, // Error:枚举成员必须具有初始化表达式
}

:::

数字枚举和字符串枚举的区别

特性数字枚举字符串枚举
默认值从 0 开始递增无默认值,每个成员需手动赋值
递增特性自增
可访问性通过枚举名或数字值访问只能通过枚举名访问
常见用途使用较多、性能较高用于表达固定字符串值

枚举的典型应用场景

枚举类型在需要定义一组固定的常量时非常有用,如方向、状态、角色等。在大型项目中,使用枚举可以让代码更具可读性,避免硬编码值(如数字或字符串)。

示例:权限角色枚举

typescript
enum Role {
  ADMIN = "admin",
  USER = "user",
  GUEST = "guest"
}

function checkAccess(role: Role) {
  if (role === Role.ADMIN) {
    console.log("拥有管理员权限")
  } else if (role === Role.USER) {
    console.log("拥有用户权限")
  } else {
    console.log("访客权限")
  }
}

checkAccess(Role.ADMIN) // 输出: 拥有管理员权限

枚举的总结与最佳实践

  • 优先使用字符串枚举:在表达固定的字符串集合时,字符串枚举更具可读性。
  • 避免魔法数字或魔法字符串:通过枚举定义常量,使代码更具可维护性和安全性。
  • 结合 never 进行 exhaustiveness checking:在 switch 语句中处理所有枚举情况时,使用 never 类型确保没有遗漏情况。 :::

Released under the MIT License.