TypeScript 与 JavaScript:您的首选指南
已发表: 2022-03-11打字稿还是 JavaScript? 开发人员考虑将这种选择用于新建 Web 或 Node.js 项目,但对于现有项目来说,这也是一个值得考虑的问题。 作为 JavaScript 的超集,TypeScript 提供了 JavaScript 的所有功能以及一些额外的好处。 TypeScript 本质上鼓励我们编写干净的代码,使代码更具可扩展性。 但是,项目可以包含尽可能多的纯 JavaScript,因此使用 TypeScript 并不是一个全有或全无的命题。
TypeScript 和 JavaScript 的关系
TypeScript 为 JavaScript 添加了显式类型系统,允许严格执行变量类型。 TypeScript 在转译时运行其类型检查——一种将 TypeScript 代码转换为 Web 浏览器和 Node.js 理解的 JavaScript 代码的编译形式。
TypeScript 与 JavaScript 示例
让我们从一个有效的 JavaScript 片段开始:
let var1 = "Hello"; var1 = 10; console.log(var1); 在这里, var1以string开始,然后变为number 。
由于 JavaScript 只是松散类型,我们可以随时将var1重新定义为任何类型的变量——从字符串到函数。
执行此代码输出10 。
现在,让我们将此代码更改为 TypeScript:
let var1: string = "Hello"; var1 = 10; console.log(var1); 在这种情况下,我们将var1声明为string 。 然后我们尝试为它分配一个数字,这是 TypeScript 严格的类型系统所不允许的。 转译会导致错误:
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; 如果我们指示转译器将原始 JavaScript 片段视为 TypeScript,转译器会自动推断var1应该是一个string | number string | number 。 这是一个 TypeScript联合类型,它允许我们随时为var1分配一个string或一个number 。 解决了类型冲突后,我们的 TypeScript 代码将成功转换。 执行它会产生与 JavaScript 示例相同的结果。
30,000 英尺高的 TypeScript 与 JavaScript:可扩展性挑战
JavaScript 无处不在,为各种规模的项目提供动力,其应用方式在 1990 年代初期是无法想象的。 虽然 JavaScript 已经成熟,但在可扩展性支持方面还不够。 因此,开发人员努力应对规模和复杂性都在增长的 JavaScript 应用程序。
值得庆幸的是,TypeScript 解决了扩展 JavaScript 项目的许多问题。 我们将专注于前三个挑战:验证、重构和文档。
验证
我们依靠集成开发环境 (IDE) 来帮助完成添加、修改和测试新代码等任务,但 IDE 无法验证纯 JavaScript 引用。 我们通过在编码时警惕地监控来缓解这个缺点,以避免变量和函数名称中出现拼写错误的可能性。
当代码来自第三方时,问题的严重性呈指数级增长,很少执行的代码分支中的损坏引用很容易未被检测到。
相比之下,使用 TypeScript,我们可以将精力集中在编码上,确信任何错误都会在编译时被识别出来。 为了证明这一点,让我们从一些遗留的 JavaScript 代码开始:
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } .toISO()调用是 moment.js toISOString()方法的错字,但如果format参数不是ISO ,代码将起作用。 当我们第一次尝试将ISO传递给函数时,它会引发这个运行时错误: TypeError: moment(...).toISO is not a function 。
定位拼写错误的代码可能很困难。 当前代码库可能没有通往断线的路径,在这种情况下,我们的损坏.toISO()引用不会被测试捕获。
如果我们将此代码移植到 TypeScript,IDE 将突出显示损坏的引用,提示我们进行更正。 如果我们什么都不做并尝试转译,我们将被阻止,转译器将产生以下错误:
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());重构
虽然第三方代码引用中的拼写错误并不少见,但内部引用中的拼写错误存在一组不同的问题,例如:
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } 唯一的开发者可以很容易地找到并修复所有以er结尾的phoneNumbr实例。
但是团队越大,这个简单、常见的错误就越会造成不合理的代价。 在执行工作的过程中,同事需要注意并传播此类拼写错误。 或者,添加支持两种拼写的代码会使代码库不必要地膨胀。
使用 TypeScript,当我们修复拼写错误时,相关代码将不再编译,通知同事将修复传播到他们的代码。
文档
准确和相关的文档是开发人员团队内部和之间沟通的关键。 JavaScript 开发人员经常使用 JSDoc 来记录预期的方法和属性类型。
TypeScript 的语言特性(例如,抽象类、接口和类型定义)促进了按合同设计的编程,从而产生了高质量的文档。 此外,对对象必须遵循的方法和属性进行正式定义有助于识别重大更改、创建测试、执行代码自省和实现架构模式。
对于 TypeScript,首选工具 TypeDoc(基于 TSDoc 提议)自动从我们的代码中提取类型信息(例如,类、接口、方法和属性)。 因此,我们毫不费力地创建了迄今为止比 JSDoc 更全面的文档。
TypeScript 与 JavaScript 的优势
现在,让我们探索如何使用 TypeScript 来解决这些可伸缩性挑战。
高级代码/重构建议
许多 IDE 可以处理来自 TypeScript 类型系统的信息,在我们编码时提供参考验证。 更好的是,当我们键入时,IDE 可以提供相关的、一目了然的文档(例如,函数期望的参数)以供任何参考,并建议上下文正确的变量名称。
在这个 TypeScript 片段中,IDE 建议自动完成函数返回值中的键名:
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }当我开始调用函数(第 31 行)时,我的 IDE Visual Studio Code 提供了这个建议(在标注中):
更重要的是,IDE 的自动完成建议(在标注中)在上下文中是正确的,仅显示嵌套键情况中的有效名称(第 34 行):
这种实时建议可以加快编码速度。 此外,IDE 可以依靠 TypeScript 严格的类型信息来重构任何规模的代码。 当我们对引用的准确性有 100% 的信心时,诸如重命名属性、更改文件位置甚至提取超类之类的操作变得微不足道。
接口支持
与 JavaScript 相比,TypeScript 提供了使用接口定义类型的能力。 接口正式列出(但不实现)对象必须包含的方法和属性。 这种语言结构对于与其他开发人员的协作特别有帮助。
下面的例子强调了我们如何利用 TypeScript 的特性来巧妙地实现常见的 OOP 模式——在这种情况下是策略和责任链——从而改进前面的例子:
export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }ES6 模块——任何地方
在撰写本文时,并非所有前端和后端 JavaScript 运行时都支持 ES6 模块。 然而,使用 TypeScript,我们可以使用 ES6 模块语法:

import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); 转译后的输出将与我们选择的环境兼容。 例如,使用编译器选项--module CommonJS ,我们得到:
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; 改用--module UMD ,TypeScript 输出更详细的 UMD 模式:
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });ES6 类——任何地方
遗留环境通常缺乏对 ES6 类的支持。 TypeScript 转译通过使用特定于目标的构造来确保兼容性。 这是一个 TypeScript 源代码片段:
export class TestClass { hello = 'World'; }JavaScript 输出取决于模块和目标,TypeScript 允许我们指定。
这是--module CommonJS --target es3产生的结果:
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; 使用--module CommonJS --target es6代替,我们得到以下转译结果。 class关键字用于针对 ES6:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;异步/等待功能——任何地方
Async/await 使异步 JavaScript 代码更易于理解和维护。 TypeScript 为所有运行时提供此功能,即使是那些不提供本地异步/等待的运行时。
请注意,要在 ES3 和 ES5 等较旧的运行时上运行 async/await,您需要对基于Promise的输出的外部支持(例如,通过 Bluebird 或 ES2015 polyfill)。 TypeScript 附带的Promise polyfill 很容易集成到转译输出中——我们只需要相应地配置lib编译器选项。
支持私有类字段——任何地方
即使对于遗留目标,TypeScript 也以与强类型语言(例如,Java 或 C#)相同的方式支持private字段。 相比之下,很多 JavaScript 运行时通过哈希前缀语法支持private字段,这是 ES2022 的一个完成提案。
TypeScript 与 JavaScript 的缺点
现在我们已经强调了实现 TypeScript 的主要好处,让我们探索 TypeScript 可能不适合的场景。
转译:工作流程不兼容的可能性
特定的工作流程或项目要求可能与 TypeScript 的转译步骤不兼容:例如,如果我们需要在部署后使用外部工具来更改代码,或者生成的输出必须对开发人员友好。
例如,我最近为 Node.js 环境编写了一个 AWS Lambda 函数。 TypeScript 不适合,因为需要转译会阻止我和其他团队成员使用 AWS 在线编辑器编辑函数。 这对项目经理来说是一个交易破坏者。
类型系统仅在转换时间之前有效
TypeScript 的 JavaScript 输出不包含类型信息,因此它不会执行类型检查,因此类型安全可能会在运行时中断。 例如,假设一个函数被定义为总是返回一个对象。 如果在.js文件中使用它返回null ,则会发生运行时错误。
依赖于类型信息的特性(例如,私有字段、接口或泛型)可以为任何项目增加价值,但在编译时会被删除。 例如, private类成员在转译后将不再是私有的。 需要明确的是,这种性质的运行时问题并不是 TypeScript 独有的,你也可以预料到 JavaScript 也会遇到同样的困难。
结合 TypeScript 和 JavaScript
尽管 TypeScript 有很多好处,但有时我们无法证明一次转换整个 JavaScript 项目是合理的。 幸运的是,我们可以逐个文件地向 TypeScript 转译器指定要解释为纯 JavaScript 的内容。 事实上,这种混合方法有助于缓解项目生命周期中出现的个别挑战。
如果以下代码,我们可能更愿意保持 JavaScript 不变:
- 由一位前同事编写,需要大量的逆向工程工作才能转换为 TypeScript。
- 使用 TypeScript 中不允许的技术(例如,在对象实例化后添加属性),并且需要重构以遵守 TypeScript 规则。
- 属于另一个继续使用 JavaScript 的团队。
在这种情况下,声明文件( .d.ts文件,有时称为定义文件或类型文件)为 TypeScript 提供了足够的类型数据来启用 IDE 建议,同时保持 JavaScript 代码不变。
许多 JavaScript 库(例如 Lodash、Jest 和 React)在单独的类型包中提供 TypeScript 类型文件,而其他库(例如 Moment.js、Axios 和 Luxon)将类型文件集成到主包中。
TypeScript 与 JavaScript:精简和可扩展性的问题
TypeScript 提供的无与伦比的支持、灵活性和增强功能显着改善了开发人员体验,使项目和团队能够扩展。 将 TypeScript 整合到项目中的主要成本是增加了转译构建步骤。 对于大多数应用程序来说,转译成 JavaScript 不是问题。 相反,它是 TypeScript 众多好处的垫脚石。
进一步阅读 Toptal 工程博客:
- 使用 TypeScript 和 Jest 支持:AWS SAM 教程
