作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Daniele Monesi
验证专家 在工程
11 的经验

Daniele是一名全栈开发人员和云解决方案架构师,曾使用过许多软件环境, 比如基于java的后端, 基于Angular和react的前端, 以及无服务器或混合云基础设施.

分享

打印稿还是JavaScript? 开发人员在greenfield web或Node中考虑这种选择.但是对于现有的项目来说,这也是一个值得考虑的问题. JavaScript的超集, 打印稿提供了JavaScript的所有特性以及一些额外的好处. 打印稿本质上鼓励我们编写干净的代码,使代码更具可扩展性. 然而,项目可以包含同样多的plain JavaScript 随我们喜欢,所以使用 打印稿 不是一个全有或全无的命题吗.

打印稿和JavaScript之间的关系

打印稿为JavaScript添加了一个显式的类型系统, 允许严格执行变量类型. 打印稿在while时运行它的类型检查 transpiling-一种编译形式,将打印稿代码转换为浏览器和Node的JavaScript代码.js理解.

打印稿vs. JavaScript的例子

让我们从一个有效的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示例相同的结果.

打印稿vs. 三万英尺的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更全面的文档.

打印稿与. JavaScript

现在,让我们探索一下如何使用打印稿来解决这些可伸缩性挑战.

高级代码/重构建议

许多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中):

在键入parse人Data()时, the IDE shows a tooltip from the 打印稿 transpiler that reads "parse人Data(data: 字符串): { 人: { name: 字符串; 姓: 字符串; 年龄: 数量; }[]; 错误: 字符串[]; }" followed by the text contained in the multiline comment before the function definition, "包含3个字段的CSV字符串, 姓, 年龄. 解析包含人员信息的CSV的简单函数.".

更重要的是, IDE的自动补全建议(在callout中)在上下文中是正确的, 只显示嵌套键情况下的有效名称(第34行):

Three suggestions (年龄, name, 和 姓) that popped up in response to typing "map(p => `Name: ${p.第一个建议被突出显示,旁边有“(财产)年龄:数字”.

这样的实时建议可以加快编码速度. 此外,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 '));
}

ES6 Modules-Anywhere

在撰写本文时,并非所有前端和后端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类放到恰当的位置

遗留环境通常缺乏对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;

异步/等待Functionality-Anywhere

异步/等待 使 异步JavaScript 代码更容易理解和维护. 打印稿为所有运行时提供了这个功能, 即使是那些不提供async/await原生程序.

请注意,要在ES3和ES5等较旧的运行时上运行async/await,您需要外部支持 承诺基于输出(e).g.(通过Bluebird或ES2015 polyfill). 的 承诺 打印稿自带的polyfill很容易集成到编译的输出中——我们只需要配置 自由 相应的编译器选项.

支持任何地方的私有类字段

即使对于遗留目标,打印稿也支持 私人 字段与强类型语言(如.g., Java或c#). 相比之下,许多JavaScript运行时支持 私人 字段通过 散列前缀 语法,这是一个完成的建议 ES2022.

打印稿与. JavaScript

现在我们已经强调了实现打印稿的主要好处, 让我们探索一下打印稿可能不适合的场景.

转译:潜在的工作流不兼容

特定的工作流程或项目需求可能与打印稿的编译步骤不兼容, 如果我们需要在部署后使用外部工具更改代码,或者生成的输出必须对开发人员友好.

例如,我最近为Node编写了一个AWS Lambda函数.js环境. 打印稿不太适合,因为需要翻译会阻止我, 以及其他团队成员, 使用AWS在线编辑器编辑函数. 对于项目经理来说,这是一个坏消息.

类型系统只在编译时工作

打印稿的JavaScript输出不包含类型信息, 所以它不会执行类型检查, 因此, 类型安全可以在运行时中断. 例如,假设一个函数被定义为总是返回一个对象. If 类型中使用后返回的 .js 文件时,将发生运行时错误.

类型信息相关的特征(例如.g., 私有字段, 接口, (或泛型)为任何项目增加价值,但在编译时被删除. 例如, 私人 类成员在编译后将不再是私有的. 澄清一下, 这种性质的运行时问题并不是打印稿独有的, 使用JavaScript也会遇到同样的困难.

结合打印稿和JavaScript

尽管打印稿有很多好处, 有时我们无法证明一次转换整个JavaScript项目是合理的. 幸运的是, 我们可以指定打印稿转译器——逐个文件地——把什么解释为纯JavaScript. 事实上, 这种混合方法可以帮助减轻在项目生命周期过程中出现的个别挑战.

如果代码:

  • 是由前同事写的,需要大量的逆向工程才能转换成打印稿.
  • 使用打印稿中不允许使用的技术(如.g., 在对象实例化后添加属性),并且需要重构以遵守打印稿规则.
  • 属于另一个继续使用JavaScript的团队.

在这种情况下,a 宣言 文件 (.d.ts 文件, (有时称为定义文件或类型文件)为打印稿提供了足够的类型数据,以便在保留JavaScript代码的同时启用IDE建议.

许多JavaScript库(如.g., Lodash, 开玩笑, 和React)在单独的类型包中提供打印稿类型文件, 而另一些人呢?.g.,时刻.js、Axios和Luxon)将类型文件集成到主包中.

打印稿vs. JavaScript:一个流线型和可伸缩性的问题

无与伦比的支持, 灵活性, 打印稿提供的增强功能极大地改善了开发者的体验, 使项目和团队能够扩展. 将打印稿整合到项目中的主要成本是增加了编译构建步骤. 对于大多数应用程序, transpiling to JavaScript is not an issue; rather, 它是打印稿的许多好处的垫脚石.

了解基本知识

  • 打印稿比JavaScript好吗?

    在语言特性方面,打印稿比JavaScript更好, 参考验证, 项目的可伸缩性, 团队内部和团队之间的协作, 开发人员的经验, 以及代码的可维护性.

  • 为什么使用打印稿而不是JavaScript?

    JavaScript在不断发展,但仍然存在代码库和开发团队规模问题,打印稿可以解决这些问题.

  • 打印稿是前端还是后端?

    打印稿代码既可以用于前端项目,也可以用于后端项目,因为它在运行前会被编译成JavaScript.

  • 打印稿的性能比JavaScript好吗?

    打印稿代码的执行通常与JavaScript代码没有任何不同, 因为它在运行之前被编译成JavaScript. 但是从开发人员性能的角度来看:是的, 打印稿让我们更容易、更快速地编写准确的代码,并在运行前捕获bug.

聘请Toptal这方面的专家.
现在雇佣
Daniele Monesi

Daniele Monesi

验证专家 在工程
11 的经验

阿尔巴诺·拉齐亚莱,意大利罗马大都会

自2021年2月1日起成为会员

作者简介

Daniele是一名全栈开发人员和云解决方案架构师,曾使用过许多软件环境, 比如基于java的后端, 基于Angular和react的前端, 以及无服务器或混合云基础设施.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

Toptal开发者

加入总冠军® 社区.