作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Daniele是一名全栈开发人员和云解决方案架构师,曾使用过许多软件环境, 比如基于java的后端, 基于Angular和react的前端, 以及无服务器或混合云基础设施.
打印稿还是JavaScript? 开发人员在greenfield web或Node中考虑这种选择.但是对于现有的项目来说,这也是一个值得考虑的问题. JavaScript的超集, 打印稿提供了JavaScript的所有特性以及一些额外的好处. 打印稿本质上鼓励我们编写干净的代码,使代码更具可扩展性. 然而,项目可以包含同样多的plain JavaScript 随我们喜欢,所以使用 打印稿 不是一个全有或全无的命题吗.
打印稿为JavaScript添加了一个显式的类型系统, 允许严格执行变量类型. 打印稿在while时运行它的类型检查 transpiling-一种编译形式,将打印稿代码转换为浏览器和Node的JavaScript代码.js理解.
让我们从一个有效的JavaScript片段开始:
let var1 = "Hello";
Var1 = 10;
控制台.日志(var1);
在这里, var1
一开始是 字符串
,然后变成 数量
.
因为JavaScript只是松散类型的,所以我们可以重新定义 var1
作为任何类型的变量—从字符串到函数—在任何时候.
执行以下代码输出 10
.
现在,让我们把这段代码改成打印稿:
let var1: 字符串 = "Hello";
Var1 = 10;
控制台.日志(var1);
在本例中,我们声明 var1
做一个 字符串
. 然后我们尝试给它赋一个数字,这是打印稿严格的类型系统所不允许的. 编译会导致错误:
TSError:无法编译打印稿
src / snippet1.ts:2:1 -错误TS2322:类型'数量'不能分配给类型'字符串'.
2 Var1 = 10;
如果我们指示转译器把原始的JavaScript片段当作打印稿来处理, 转译器会自动推断出这一点 var1
应该是。 字符串|数字
. 这是一个打印稿 联合类型,它允许我们分配 var1
a 字符串
或者一个 数量
在任何时候. 解决了类型冲突后,我们的打印稿代码就可以成功编译了. 执行它将产生与JavaScript示例相同的结果.
JavaScript无处不在, 为各种规模的项目提供动力, 在20世纪90年代的初期,这种应用方式是不可想象的. 虽然JavaScript已经成熟,但它在可伸缩性支持方面还存在不足. 相应的, 开发人员正在努力应对规模和复杂性都在增长的JavaScript应用程序.
值得庆幸的是,打印稿解决了许多扩展JavaScript项目的问题. 我们将重点关注前三个挑战:验证、重构和文档.
我们依靠集成开发环境(ide)来帮助完成添加之类的任务, 修改, 测试新代码, 但是ide不能验证纯JavaScript引用. 我们在编写代码时小心地监视,以避免变量和函数名中出现错别字的可能性,从而减轻了这个缺点.
当代码来自第三方时,问题的严重性会呈指数级增长, 在很少执行的代码分支中损坏的引用很容易不被发现的地方.
与此形成鲜明对比的是, 与打印稿, 我们可以把精力集中在编码上, 确信任何错误都会在编译时被识别出来. 为了证明这一点,让我们从一些开始 遗产 JavaScript代码:
Const moment = require('moment');
const printCurrentTime = (格式) => {
if (格式 === 'ISO'){
控制台.log("当前ISO TS:", moment()).toISO ());
} else {
控制台.log("当前TS: ", moment()).格式(格式));
}
}
的 .toISO ()
Call是一时的错别字.js toISOString ()
方法,但只要提供 格式
参数不 ISO
. 我们第一次试图通过 ISO
对于函数,它将引发以下运行时错误: TypeError:时刻(...).toISO不是一个函数
.
查找拼写错误的代码可能很困难. 当前代码库可能没有到断行的路径,在这种情况下,我们的断行 .toISO ()
引用不会被测试捕获.
如果我们把这段代码移植到打印稿中, IDE将突出显示损坏的引用, 促使我们做出纠正. 如果我们什么都不做,试图翻译, 我们会被封锁, 转译器会生成以下错误:
TSError:无法编译打印稿
src / catching-mistakes-at-compile-time.ts:5:49 -错误TS2339:属性'toISO'不存在类型'Moment'.
5控制台.log("当前ISO TS:", moment()).toISO ());
而第三方代码引用中的错别字并不少见, 内部引用中的错别字有一系列不同的问题, 比如这个:
const myPhoneFunction = (opts) => {
// ...
如果选择.phoneNumbr)
doStuff ();
}
的所有实例都可以由单独的开发人员定位和修复 phoneNumbr
最后 er
足够容易.
但是,团队规模越大,这个简单而常见的错误造成的成本就越高. 在执行工作的过程中, 同事们需要注意并传播这些错别字. 另外,添加代码来支持两种拼写会不必要地增加代码库.
与打印稿, 当我们修正错别字时, 依赖的代码将不再被编译, 通知同事将修复传播到他们的代码中.
准确和相关的文档是开发团队内部和团队之间沟通的关键. JavaScript开发人员经常使用JSDoc来记录期望的方法和属性类型.
打印稿的语言特性(例如.g., 抽象类, 接口, 和类型定义)促进了契约式设计编程, 生成高质量的文档. 此外, 拥有对象必须遵循的方法和属性的正式定义有助于识别破坏性更改, 创建测试, 执行代码自省, 实现架构模式.
对于打印稿, go-to工具 TypeDoc (基于 TSDoc 建议)自动提取类型信息(例如.g.(类、接口、方法和属性). 因此,我们毫不费力地创建了迄今为止比JSDoc更全面的文档.
现在,让我们探索一下如何使用打印稿来解决这些可伸缩性挑战.
许多ide可以处理来自打印稿类型系统的信息, 在编写代码时提供引用验证. 更好的是,当我们键入时,IDE可以提供相关的、一目了然的文档(例如.g., 函数期望的参数)用于任何引用,并建议上下文正确的变量名称.
在这个打印稿片段中, IDE建议自动补全函数返回值中的键名:
/**
*简单的函数来解析包含人员信息的CSV.
* @param data一个包含3个字段的CSV字符串:姓名,年龄.
*/
const parse人Data = (data: 字符串) => {
Const 人:{姓名:字符串,姓氏:字符串,年龄:数字}[]= [];
Const 错误: 字符串[] = [];
对于(let)一行数据.分割(' \ n ')) {
如果(行.Trim() === ")继续;
Const token = row.分割(" ").map(i => i.削减()).filter(i => i != '');
如果令牌.length < 3){
错误.(推' Row "${Row}"只包含${标记.长度}标记. 3要求);
继续;
}
人.Push ({name: tokens[0],姓:token[1],年龄:+token [2]})
}
返回{人, 错误};
};
const exampleData = '
戈登弗里曼27
G,人,99年
Alyx,万斯,24岁
无效的行,,
再次,无效
`;
const result = parse人Data(exampleData);
控制台.日志(“解析人:”);
控制台.日志(结果.人.
map(p => `Name: ${p.名称}\ nSurname: $ {p.姓}\内奇:$ {p.年龄}”)
.加入(“\ n \ n”)
);
如果结果.错误.length > 0){
控制台.日志(“\ nErrors:”);
控制台.日志(结果.错误.加入(' \ n '));
}
我的IDE, Visual Studio代码, 当我开始调用函数(第31行)时,提供了这个建议(在callout中):
更重要的是, IDE的自动补全建议(在callout中)在上下文中是正确的, 只显示嵌套键情况下的有效名称(第34行):
这样的实时建议可以加快编码速度. 此外,ide可以依赖打印稿严格的类型信息来重构任何规模的代码. 重命名属性等操作, 更改文件位置, 或者,当我们对引用的准确性有100%的信心时,甚至提取超类也变得微不足道.
与JavaScript相比,打印稿提供了使用定义类型的能力 接口. 接口正式列出——但不实现——对象必须包含的方法和属性. 这种语言结构对于与其他开发人员协作特别有帮助.
下面的例子强调了我们如何利用打印稿的特性来整齐地实现常见的OOP模式——在这个例子中, 策略 和 责任链-从而改进了前面的例子:
导出类PersonInfo {
构造函数(
公共名称:字符串;
公众姓氏:字符串;
公众年龄:
){}
}
导出接口ParserStrategy
/**
*如果可以,解析一行.
* @返回已解析的行,如果格式无法识别则返回零.
*/
(line: 字符串): PersonInfo | 零;
}
导出类PersonInfoParser{
public 策略: ParserStrategy[] = [];
解析(数据:字符串){
const 人: PersonInfo[] = [];
Const 错误: 字符串[] = [];
对于(let)一行数据.分割(' \ n ')) {
如果(行.Trim() === ")继续;
让解析;
让我们来看看这个.策略){
解析= s(行);
If(解析)break;
}
if (!解析){
错误.(推'无法找到能够解析“${row}”的策略');
} else {
人.推动(解析);
}
}
返回{人, 错误};
}
}
const exampleData = '
戈登弗里曼27
G;人;99
{"name":"Alyx", "姓":"Vance", "年龄":24}
无效的行,,
再次,无效
`;
const 解析器 = new PersonInfoParser();
const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => {
Const token = line.分割(fieldSeparator).map(i => i.削减()).filter(i => i != '');
如果令牌.length < 3) 返回零;
返回新的PersonInfo(tokens[0], token [1], +token [2]);
};
解析器.策略.(推
(line) => {
尝试{
const{姓名,年龄}= JSON.解析(线);
返回新的PersonInfo(姓名,年龄);
}捕捉(err) {
返回零;
}
},
createCSVStrategy (),
createCSVStrategy(“;”)
);
Const result =解析器.解析(exampleData);
控制台.日志(“解析人:”);
控制台.日志(结果.人.
map(p => `Name: ${p.名称}\ nSurname: $ {p.姓}\内奇:$ {p.年龄}”)
.加入(“\ n \ n”)
);
如果结果.错误.length > 0){
控制台.日志(“\ nErrors:”);
控制台.日志(结果.错误.加入(' \ n '));
}
在撰写本文时,并非所有前端和后端JavaScript运行时都支持ES6模块. 然而,在打印稿中,我们可以使用ES6模块语法:
从'lodash'中导入* as _;
export const exampleFn = () => 控制台.日志(_.Reverse (['a', 'b', 'c']));
编译后的输出将与我们选择的环境兼容. 例如,使用编译器选项 ——模块CommonJS
,我们得到:
“使用严格的”;
出口.__esModule = true;
出口.exampleFn = void 0;
Var _ = require("lodash");
var exampleFn = function(){返回控制台.日志(_.Reverse (['a', 'b', 'c'])); };
出口.exampleFn = exampleFn;
使用 ——模块UMD格式
相反,打印稿会输出更详细的UMD模式:
(function (factory) {
If (typeof模块 === "object") && typeof模块.导出=== "对象"){
Var v = factory(require, 出口);
如果(v !== un定义d)模块.Exports = v;
}
if (typeof 定义 === "function") && 定义.amd) {
定义(["require", "出口", "lodash"], factory);
}
})(function (require, 出口) {
“使用严格的”;
出口.__esModule = true;
出口.exampleFn = void 0;
Var _ = require("lodash");
var exampleFn = function(){返回控制台.日志(_.Reverse (['a', 'b', 'c'])); };
出口.exampleFn = exampleFn;
});
遗留环境通常缺乏对ES6类的支持. 打印稿编译通过使用特定于目标的结构来确保兼容性. 下面是打印稿的源代码片段:
导出类TestClass {
hello = 'World';
}
JavaScript输出依赖于这两者 模块和目标, 打印稿允许我们指定它.
这就是 ——module CommonJS——target es3
收益率:
“使用严格的”;
出口.__esModule = true;
出口.TestClass = void 0;
var TestClass = /** @class */ (function () {
TestClass() {
这.hello = 'World';
}
返回TestClass;
}());
出口.TestClass = TestClass;
使用 ——module CommonJS——target es6
相反,我们得到以下编译结果. 的 class
关键字用于瞄准ES6:
“使用严格的”;
Object.定义Property(出口, "__esModule", {value: true});
出口.TestClass = void 0;
类TestClass {
构造函数(){
这.hello = 'World';
}
}
出口.TestClass = TestClass;
异步/等待 使 异步JavaScript 代码更容易理解和维护. 打印稿为所有运行时提供了这个功能, 即使是那些不提供async/await原生程序.
请注意,要在ES3和ES5等较旧的运行时上运行async/await,您需要外部支持 承诺
基于输出(e).g.(通过Bluebird或ES2015 polyfill). 的 承诺
打印稿自带的polyfill很容易集成到编译的输出中——我们只需要配置 自由
相应的编译器选项.
即使对于遗留目标,打印稿也支持 私人
字段与强类型语言(如.g., Java或c#). 相比之下,许多JavaScript运行时支持 私人
字段通过 散列前缀 语法,这是一个完成的建议 ES2022.
现在我们已经强调了实现打印稿的主要好处, 让我们探索一下打印稿可能不适合的场景.
特定的工作流程或项目需求可能与打印稿的编译步骤不兼容, 如果我们需要在部署后使用外部工具更改代码,或者生成的输出必须对开发人员友好.
例如,我最近为Node编写了一个AWS Lambda函数.js环境. 打印稿不太适合,因为需要翻译会阻止我, 以及其他团队成员, 使用AWS在线编辑器编辑函数. 对于项目经理来说,这是一个坏消息.
打印稿的JavaScript输出不包含类型信息, 所以它不会执行类型检查, 因此, 类型安全可以在运行时中断. 例如,假设一个函数被定义为总是返回一个对象. If 零
类型中使用后返回的 .js
文件时,将发生运行时错误.
类型信息相关的特征(例如.g., 私有字段, 接口, (或泛型)为任何项目增加价值,但在编译时被删除. 例如, 私人
类成员在编译后将不再是私有的. 澄清一下, 这种性质的运行时问题并不是打印稿独有的, 使用JavaScript也会遇到同样的困难.
尽管打印稿有很多好处, 有时我们无法证明一次转换整个JavaScript项目是合理的. 幸运的是, 我们可以指定打印稿转译器——逐个文件地——把什么解释为纯JavaScript. 事实上, 这种混合方法可以帮助减轻在项目生命周期过程中出现的个别挑战.
如果代码:
在这种情况下,a 宣言 文件 (.d.ts
文件, (有时称为定义文件或类型文件)为打印稿提供了足够的类型数据,以便在保留JavaScript代码的同时启用IDE建议.
许多JavaScript库(如.g., Lodash, 开玩笑, 和React)在单独的类型包中提供打印稿类型文件, 而另一些人呢?.g.,时刻.js、Axios和Luxon)将类型文件集成到主包中.
无与伦比的支持, 灵活性, 打印稿提供的增强功能极大地改善了开发者的体验, 使项目和团队能够扩展. 将打印稿整合到项目中的主要成本是增加了编译构建步骤. 对于大多数应用程序, transpiling to JavaScript is not an issue; rather, 它是打印稿的许多好处的垫脚石.
在语言特性方面,打印稿比JavaScript更好, 参考验证, 项目的可伸缩性, 团队内部和团队之间的协作, 开发人员的经验, 以及代码的可维护性.
JavaScript在不断发展,但仍然存在代码库和开发团队规模问题,打印稿可以解决这些问题.
打印稿代码既可以用于前端项目,也可以用于后端项目,因为它在运行前会被编译成JavaScript.
打印稿代码的执行通常与JavaScript代码没有任何不同, 因为它在运行之前被编译成JavaScript. 但是从开发人员性能的角度来看:是的, 打印稿让我们更容易、更快速地编写准确的代码,并在运行前捕获bug.
阿尔巴诺·拉齐亚莱,意大利罗马大都会
自2021年2月1日起成为会员
Daniele是一名全栈开发人员和云解决方案架构师,曾使用过许多软件环境, 比如基于java的后端, 基于Angular和react的前端, 以及无服务器或混合云基础设施.
世界级的文章,每周发一次.
世界级的文章,每周发一次.