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

Daniele Monesi

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

工作经验

11

Share

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

TypeScript和JavaScript之间的关系

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

打印稿vs. JavaScript的例子

让我们从一个有效的JavaScript片段开始:

let var1 = "Hello";
var1 = 10;
console.log(var1); 

Here, var1 一开始是 string,然后变成 number.

因为JavaScript只是松散类型的,所以我们可以重新定义 var1 作为任何类型的变量—从字符串到函数—在任何时候.

执行以下代码输出 10.

现在,让我们把这段代码改成TypeScript:

let var1: string = "Hello";
var1 = 10;
console.log(var1);

在本例中,我们声明 var1 to be a string. 然后我们尝试给它赋一个数字,这是TypeScript严格的类型系统所不允许的. 编译会导致错误:

TSError:无法编译TypeScript
src/snippet1.ts:2:1 -错误TS2322:类型'number'不能分配给类型'string'.

2 var1 = 10;

如果我们指示转译器把原始的JavaScript片段当作TypeScript来处理, 转译器会自动推断出这一点 var1 should be a 字符串|数字. 这是一个TypeScript union type,它允许我们分配 var1 a string or a number at any time. 解决了类型冲突后,我们的TypeScript代码就可以成功编译了. 执行它将产生与JavaScript示例相同的结果.

打印稿vs. 三万英尺的JavaScript:可扩展性挑战

JavaScript无处不在, 为各种规模的项目提供动力, 在20世纪90年代的初期,这种应用方式是不可想象的. 虽然JavaScript已经成熟,但它在可伸缩性支持方面还存在不足. Accordingly, 开发人员正在努力应对规模和复杂性都在增长的JavaScript应用程序.

值得庆幸的是,TypeScript解决了许多扩展JavaScript项目的问题. 我们将重点关注前三个挑战:验证、重构和文档.

Validation

我们依靠集成开发环境(ide)来帮助完成添加之类的任务, modifying, 测试新代码, 但是ide不能验证纯JavaScript引用. 我们在编写代码时小心地监视,以避免变量和函数名中出现错别字的可能性,从而减轻了这个缺点.

当代码来自第三方时,问题的严重性会呈指数级增长, 在很少执行的代码分支中损坏的引用很容易不被发现的地方.

In contrast, 与打印稿, 我们可以把精力集中在编码上, 确信任何错误都会在编译时被识别出来. 为了证明这一点,让我们从一些开始 legacy JavaScript代码:

Const moment = require('moment');

const printCurrentTime = (format) => {
    if (format === 'ISO'){
        console.log("当前ISO TS:", moment()).toISO()); 
    } else {
        console.log("当前TS: ", moment()).格式(格式));
    }
}

The .toISO() Call是一时的错别字.js toISOString () 方法,但只要提供 format 参数不 ISO. 我们第一次试图通过 ISO 对于函数,它将引发以下运行时错误: TypeError:时刻(...).toISO不是一个函数.

查找拼写错误的代码可能很困难. 当前代码库可能没有到断行的路径,在这种情况下,我们的断行 .toISO() 引用不会被测试捕获.

如果我们把这段代码移植到TypeScript中, IDE将突出显示损坏的引用, 促使我们做出纠正. 如果我们什么都不做,试图翻译, 我们会被封锁, 转译器会生成以下错误:

TSError:无法编译TypeScript
src / catching-mistakes-at-compile-time.ts:5:49 -错误TS2339:属性'toISO'不存在类型'Moment'.

5控制台.log("当前ISO TS:", moment()).toISO());

Refactoring

而第三方代码引用中的错别字并不少见, 内部引用中的错别字有一系列不同的问题, 比如这个:

const myPhoneFunction = (opts) => {
    // ...
    if (opts.phoneNumbr)
        doStuff();
}

的所有实例都可以由单独的开发人员定位和修复 phoneNumbr to end with er 足够容易.

但是,团队规模越大,这个简单而常见的错误造成的成本就越高. 在执行工作的过程中, 同事们需要注意并传播这些错别字. 另外,添加代码来支持两种拼写会不必要地增加代码库.

与打印稿, 当我们修正错别字时, 依赖的代码将不再被编译, 通知同事将修复传播到他们的代码中.

文档

准确和相关的文档是开发团队内部和团队之间沟通的关键. JavaScript开发人员经常使用JSDoc来记录期望的方法和属性类型.

TypeScript的语言特性(例如.g., 抽象类, interfaces, 和类型定义)促进了契约式设计编程, 生成高质量的文档. Moreover, 拥有对象必须遵循的方法和属性的正式定义有助于识别破坏性更改, create tests, 执行代码自省, 实现架构模式.

对于TypeScript, go-to工具 TypeDoc (基于 TSDoc 建议)自动提取类型信息(例如.g.(类、接口、方法和属性). 因此,我们毫不费力地创建了迄今为止比JSDoc更全面的文档.

TypeScript与. JavaScript

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

高级代码/重构建议

许多ide可以处理来自TypeScript类型系统的信息, 在编写代码时提供引用验证. 更好的是,当我们键入时,IDE可以提供相关的、一目了然的文档(例如.g., 函数期望的参数)用于任何引用,并建议上下文正确的变量名称.

在这个TypeScript片段中, IDE建议自动补全函数返回值中的键名:

/**
 *简单的函数来解析包含人员信息的CSV.
 * @param data一个包含3个字段的CSV字符串:姓名,年龄.
 */
const parsePeopleData = (data: string) => {
    Const people:{姓名:字符串,姓氏:字符串,年龄:数字}[]= [];
    Const errors: string[] = [];

    对于(let)一行数据.分割(' \ n ')) {
        if (row.Trim() === ")继续;

        Const token = row.split(',').map(i => i.trim()).filter(i => i != '');
        if (tokens.length < 3){
            errors.push(' Row "${Row}"只包含${标记.长度}标记. 3要求);
            continue;
        }
        people.Push ({name: tokens[0],姓:token[1],年龄:+token [2]})
    }
    返回{people, errors};
};


const exampleData = '
    戈登弗里曼27
    G,Man,99
    Alyx,万斯,24岁
    无效的行,,
    再次,无效
`;

const result = parsePeopleData(exampleData);
console.日志(“解析人:”);
console.log(result.people.
                map(p => `Name: ${p.名称}\ nSurname: $ {p.姓}\内奇:$ {p.age}`)
                .join('\n\n')
);
if (result.errors.length > 0){
    console.日志(“\ nErrors:”);
    console.log(result.errors.join('\n'));
}

My IDE, Visual Studio代码, 当我开始调用函数(第31行)时,提供了这个建议(在callout中):

在键入parsePeopleData()时, the IDE shows a tooltip from the TypeScript transpiler that reads "parsePeopleData(data: string): { people: { name: string; surname: string; age: number; }[]; errors: string[]; }" followed by the text contained in the multiline comment before the function definition, "包含3个字段的CSV字符串, surname, age. 解析包含人员信息的CSV的简单函数.".

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

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

这样的实时建议可以加快编码速度. 此外,ide可以依赖TypeScript严格的类型信息来重构任何规模的代码. 重命名属性等操作, 更改文件位置, 或者,当我们对引用的准确性有100%的信心时,甚至提取超类也变得微不足道.

接口支持

与JavaScript相比,TypeScript提供了使用定义类型的能力 interfaces. 接口正式列出——但不实现——对象必须包含的方法和属性. 这种语言结构对于与其他开发人员协作特别有帮助.

下面的例子强调了我们如何利用TypeScript的特性来整齐地实现常见的OOP模式——在这个例子中, strategy and 责任链-从而改进了前面的例子:

导出类PersonInfo {
    constructor(
        公共名称:字符串; 
        公众姓氏:string; 
        公众年龄:
    ){}
}

导出接口ParserStrategy
    /**
     *如果可以,解析一行.
     * @返回已解析的行,如果格式无法识别则返回null.
     */
    (line: string): PersonInfo | null;
}

导出类PersonInfoParser{

    public strategies: ParserStrategy[] = [];

    解析(数据:字符串){
        const people: PersonInfo[] = [];
        Const errors: string[] = [];

        对于(let)一行数据.分割(' \ n ')) {
            if (row.Trim() === ")继续;

            let parsed;
            让我们来看看这个.strategies){
                解析= s(行);
                If(解析)break;
            }
            if (!parsed){
                errors.push('无法找到能够解析“${row}”的策略');
            } else {
                people.推动(解析);
            }
        }
        返回{people, errors};
    }
}


const exampleData = '
    戈登弗里曼27
    G;Man;99
    {"name":"Alyx", "姓":"Vance", "age":24}
    无效的行,,
    再次,无效
`;

const parser = new PersonInfoParser();

const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => {
    Const token = line.分割(fieldSeparator).map(i => i.trim()).filter(i => i != '');
    if (tokens.length < 3) return null;
    返回新的PersonInfo(tokens[0], token [1], +token [2]);
};

parser.strategies.push(
    (line) => {
        try {
            const{姓名,年龄}= JSON.parse(line);
            返回新的PersonInfo(姓名,年龄);
        }捕捉(err) {
            return null;
        }
    },
    createCSVStrategy (),
    createCSVStrategy(“;”)
);

Const result =解析器.解析(exampleData);
console.日志(“解析人:”);
console.log(result.people.
                map(p => `Name: ${p.名称}\ nSurname: $ {p.姓}\内奇:$ {p.age}`)
                .join('\n\n')
);
if (result.errors.length > 0){
    console.日志(“\ nErrors:”);
    console.log(result.errors.join('\n'));
}

ES6 Modules-Anywhere

在撰写本文时,并非所有前端和后端JavaScript运行时都支持ES6模块. 然而,在TypeScript中,我们可以使用ES6模块语法:

从'lodash'中导入* as _;
export const exampleFn = () => console.log(_.Reverse (['a', 'b', 'c']));

编译后的输出将与我们选择的环境兼容. 例如,使用编译器选项 ——模块CommonJS, we get:

“使用严格的”;
exports.__esModule = true;
exports.exampleFn = void 0;
Var _ = require("lodash");
var exampleFn = function(){返回控制台.log(_.Reverse (['a', 'b', 'c'])); };
exports.exampleFn = exampleFn;

Using --module UMD 相反,TypeScript会输出更详细的UMD模式:

(function (factory) {
    If (typeof模块 === "object") && typeof模块.导出=== "对象"){
        Var v = factory(require, exports);
        if (v !== undefined)模块.exports = v;
    }
    if (typeof define === "function") && define.amd) {
        定义(["require", "exports", "lodash"], factory);
    }
})(function (require, exports) {
    “使用严格的”;
    exports.__esModule = true;
    exports.exampleFn = void 0;
    Var _ = require("lodash");
    var exampleFn = function(){返回控制台.log(_.Reverse (['a', 'b', 'c'])); };
    exports.exampleFn = exampleFn;
});

ES6类放到恰当的位置

遗留环境通常缺乏对ES6类的支持. TypeScript编译通过使用特定于目标的结构来确保兼容性. 下面是TypeScript的源代码片段:

导出类TestClass {
    hello = 'World';
}

JavaScript输出依赖于这两者 模块和目标, TypeScript允许我们指定它.

Here’s what ——module CommonJS——target es3 yields:

“使用严格的”;
exports.__esModule = true;
exports.TestClass = void 0;
var TestClass = /** @class */ (function () {
    TestClass() {
        this.hello = 'World';
    }
    返回TestClass;
}());
exports.TestClass = TestClass;

Using ——module CommonJS——target es6 相反,我们得到以下编译结果. The class 关键字用于瞄准ES6:

“使用严格的”;
Object.defineProperty(exports, "__esModule", {value: true});
exports.TestClass = void 0;
类TestClass {
    构造函数(){
        this.hello = 'World';
    }
}
exports.TestClass = TestClass;

异步/等待Functionality-Anywhere

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

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

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

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

TypeScript与. JavaScript

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

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

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

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

类型系统只在编译时工作

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

类型信息相关的特征(例如.g., 私有字段, interfaces, (或泛型)为任何项目增加价值,但在编译时被删除. For example, private 类成员在编译后将不再是私有的. To be clear, 这种性质的运行时问题并不是TypeScript独有的, 使用JavaScript也会遇到同样的困难.

结合TypeScript和JavaScript

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

如果代码:

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

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

许多JavaScript库(如.g., Lodash, Jest, 和React)在单独的类型包中提供TypeScript类型文件, 而另一些人呢?.g., Moment.js、Axios和Luxon)将类型文件集成到主包中.

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

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

关于总博客的进一步阅读:

了解基本知识

  • TypeScript比JavaScript好吗?

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

  • 为什么使用TypeScript而不是JavaScript?

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

  • TypeScript是前端还是后端?

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

  • TypeScript的性能比JavaScript好吗?

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

聘请Toptal这方面的专家.
Hire Now
Daniele Monesi的头像
Daniele Monesi

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

Member since 2021年2月1日

作者简介

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

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

工作经验

11

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

订阅意味着同意我们的 隐私政策

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

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® community.