[TypeScript] 编程实践之1: Google的TypeScript代码风格1:介绍

版本1.8

2016年1月

Microsoft自2012年10月1日起根据Open Web Foundation最终规范协议版本1.0(“ OWF 1.0”)提供此规范。OWF 1.0可以从http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0获得。

TypeScript是Microsoft Corporation的注册商标。

1 介绍

诸如Web电子邮件,地图,文档编辑和协作工具之类的JavaScript应用程序已成为日常计算中越来越重要的部分。我们设计TypeScript来满足构建和维护大型JavaScript程序的JavaScript编程团队的需求。 TypeScript帮助编程团队定义软件组件之间的接口,并深入了解现有JavaScript库的行为。 TypeScript还使团队可以通过将其代码组织到可动态加载的模块中来减少命名冲突。 TypeScript的可选类型系统使JavaScript程序员能够使用高效的开发工具和实践:静态检查,基于符号的导航,语句完成和代码重构。

TypeScript是JavaScript的语法糖。 TypeScript语法是ECMAScript 2015(ES2015)语法的超集。每个JavaScript程序也是TypeScript程序。 TypeScript编译器仅对TypeScript程序执行文件本地转换,并且不对TypeScript中声明的变量重新排序。这将导致JavaScript输出与TypeScript输入非常匹配。 TypeScript不会转换变量名,从而使直接调试生成的JavaScript变得容易。 TypeScript可以选择提供源映射,从而启用源级别的调试。 TypeScript工具通常在保存文件时生成JavaScript,从而保留了JavaScript开发中通常使用的测试,编辑,刷新周期。

TypeScript语法包含ECMAScript 2015的所有功能,包括类和模块,并提供将这些功能转换为ECMAScript 3或5兼容代码的功能。

类使程序员能够以标准方式表达常见的面向对象的模式,从而使继承等功能更易读和可互操作。模块使程序员可以将代码组织到组件中,同时避免命名冲突。 TypeScript编译器提供了模块代码生成选项,这些选项支持静态或动态加载模块内容。

TypeScript还为JavaScript程序员提供了可选类型注释的系统。这些类型注释类似于在Closure系统中找到的JSDoc注释,但是在TypeScript中,它们直接集成到语言语法中。这种集成使代码更具可读性,并减少了将类型注释与其相应变量同步的维护成本。

TypeScript类型系统使程序员能够表达对JavaScript对象功能的限制,并使用强制执行这些限制的工具。为了最大程度地减少工具变得有用所需的注释数量,TypeScript类型系统广泛使用类型推断。例如,从以下语句中,TypeScript将推断变量’i’具有number类型。

var i = 0;

TypeScript将根据以下函数定义推断函数f具有string返回类型。

function f() {  
    return "hello";  
}

为了从这种推断中受益,程序员可以使用TypeScript语言服务。 例如,代码编辑器可以合并TypeScript语言服务,并使用该服务来查找字符串对象的成员,如以下屏幕截图所示。
在这里插入图片描述
在此示例中,程序员无需提供类型注释即可受益于类型推断。 但是,某些有益的工具确实需要程序员提供类型注释。 在TypeScript中,我们可以像下面的代码片段中那样表达参数要求。

function f(s: string) {  
    return s;  
}

f({});       // Error  
f("hello");  // Ok

参数s上的此可选类型注释使TypeScript类型检查器知道程序员期望参数s的类型为“字符串”。 在函数“ f”的主体内,工具可以假定“ s”的类型为“字符串”,并提供与该假设一致的操作类型检查和成员变量自动完成。 工具也可以在第一次调用“ f”时生成错误信号,因为“ f”需要字符串而不是对象作为参数。 对于函数“ f”,TypeScript编译器将生成以下JavaScript代码:

function f(s) {  
    return s;  
}

在JavaScript输出中,所有类型注释均已删除。 通常,TypeScript在生成JavaScript之前会擦除所有类型信息。

1.1 环境声明

环境声明将变量引入TypeScript范围,但对生成的JavaScript程序的影响为零。 程序员可以使用环境声明来告诉TypeScript编译器某些其他组件将提供变量。 例如,默认情况下,TypeScript编译器将为使用未定义变量打印错误。 要添加浏览器定义的一些公共变量,TypeScript程序员可以使用环境声明。 下面的示例声明浏览器提供的“document”对象。 因为声明未指定类型,所以推断类型为“ any”。 “any”类型意味着工具不能对document对象的形状或行为承担任何责任。 下面的一些示例将说明程序员如何使用类型来进一步表征对象的预期行为。

declare var document;  
document.title = "Hello";  // Ok because document has been declared

对于“document”,TypeScript编译器会自动提供一个声明,因为默认情况下,TypeScript包含一个文件“ lib.d.ts”,该文件为内置JavaScript库以及文档对象模型提供了接口声明。

TypeScript编译器默认不包括jQuery的接口,因此要使用jQuery,程序员可以提供如下声明:

declare var $;

第1.3节提供了一个更广泛的示例,说明程序员如何为jQuery和其他库添加类型信息。

1.2 函数类型

函数表达式是JavaScript的强大功能。 它们使函数定义能够创建闭包:从围绕函数定义的词法范围中捕获信息的函数。 当前,闭包是JavaScript强制执行数据封装的唯一方法。 通过捕获和使用环境变量,闭包可以保留无法从闭包外部访问的信息。 JavaScript程序员经常使用闭包来表示事件处理程序和其它异步回调,其中另一个软件组件(例如DOM)将通过处理程序函数回调JavaScript。

TypeScript函数类型使程序员可以表达函数的预期签名。 函数签名是参数类型加返回类型的序列。 下面的示例使用函数类型来表达异步vote机制的回调签名要求。

function vote(candidate: string, callback: (result: string) => any) {  
   // ...  
}

vote("BigPig",  
     function(result: string) {  
         if (result === "BigPig") {  
            // ...  
         }  
     }  
);

在此示例中,“ vote”的第二个参数具有函数类型

(result: string) => any

这意味着第二个参数是一个返回“ any”类型的函数,该函数具有一个名为“ result”的“ string”类型的单个参数。第3.9.2节提供了有关函数类型的其他信息。

1.3 对象类型

TypeScript程序员使用对象类型来声明他们对对象行为的期望。 以下代码使用对象类型文字来指定’MakePoint’函数的返回类型。

var MakePoint: () => {  
    x: number; y: number;  
};

程序员可以给对象类型起名字; 我们称为命名对象类型接口。 例如,在以下代码中,接口声明一个必填字段(name)和一个可选字段(favoriteColor)。

interface Friend {  
    name: string;  
    favoriteColor?: string;  
}

function add(friend: Friend) {  
    var name = friend.name;  
}

add({ name: "Fred" });  // Ok  
add({ favoriteColor: "blue" });  // Error, name required  
add({ name: "Jill", favoriteColor: "green" });  // Ok

TypeScript对象类型对JavaScript对象可以表现的行为多样性进行建模。 例如,jQuery库定义一个对象“ $”,该对象具有诸如“ get”(发送Ajax消息)之类的方法以及诸如“ browser”(向浏览器供应商提供信息)之类的字段。 但是,jQuery客户端也可以将“ $”作为函数调用。 该函数的行为取决于传递给该函数的参数的类型。

以下代码片段捕获了jQuery行为的一小部分,足以以一种简单的方式使用jQuery。

interface JQuery {  
    text(content: string);  
}  
  
interface JQueryStatic {  
    get(url: string, callback: (data: string) => any);     
    (query: string): JQuery;  
}

declare var $: JQueryStatic;

$.get("http://mysite.org/divContent",  
      function (data: string) {  
          $("div").text(data);  
      }  
);

“ JQueryStatic”接口引用另一个接口:“ JQuery”。 此接口表示一个或多个DOM元素的集合。 jQuery库可以对这样的集合执行许多操作,但是在此示例中,jQuery客户端仅需要知道它可以通过将字符串传递给’text’方法来设置集合中每个jQuery元素的文本内容。 “ JQueryStatic”接口还包含“ get”方法,该方法对提供的URL执行Ajax get操作,并安排在收到响应后调用提供的回调。

最后,“ JQueryStatic”接口包含一个裸函数签名

(query: string): JQuery;

裸签名表示该接口的实例是可调用的。 此示例说明TypeScript函数类型只是TypeScript对象类型的特例。 具体来说,函数类型是包含一个或多个调用签名的对象类型。 因此,我们可以将任何函数类型编写为对象类型文字。 下面的示例使用两种形式描述相同的类型。

var f: { (): string; };  
var sameType: () => string = f;     // Ok  
var nope: () => number = sameType;  // Error: type mismatch

上面我们提到,’$'函数的行为取决于其参数的类型。 到目前为止,我们的jQuery输入仅捕获以下行为之一:传递字符串时返回类型为’JQuery’的对象。 为了指定多种行为,TypeScript支持在对象类型中重载函数签名。 例如,我们可以向’JQueryStatic’接口添加一个附加的呼叫签名。

(ready: () => any): any;

此签名表示可以将一个函数作为’$‘函数的参数传递。 当函数传递给’$'时,当DOM文档准备就绪时,jQuery库将调用该函数。 因为TypeScript支持重载,所以工具可以使用TypeScript来显示所有可用的函数签名及其文档提示,并在使用特定签名调用函数后提供正确的文档。

典型的客户不需要添加任何其他类型,而只需使用社区提供的类型来发现(通过带有文档提示的语句完成)并(通过静态检查)验证库的正确使用,如以下屏幕截图所示。
在这里插入图片描述
第3.9.2节提供了有关对象类型的其他信息。

1.4 结构子类型化

对象类型会进行结构化的比较。 例如,在下面的代码片段中,类“ CPoint”与接口“ Point”匹配,因为“ CPoint”具有“ Point”的所有必需成员。 一个类可以选择声明它实现了一个接口,以便编译器将检查该声明的结构兼容性。 该示例还说明,对象类型可以匹配从对象文字推断出的类型,只要该对象文字提供了所有必需的成员。

interface Point {  
    x: number;  
    y: number;  
}

function getX(p: Point) {  
    return p.x;  
}

class CPoint {  
    x: number;  
    y: number;  
    constructor(x: number,  y: number) {  
        this.x = x;  
        this.y = y;  
    }  
}

getX(new CPoint(0, 0));  // Ok, fields match

getX({ x: 0, y: 0, color: "red" });  // Extra fields Ok

getX({ x: 0 });  // Error: supplied parameter does not match

有关类型比较的更多信息,请参见第3.3节

1.5 上下文类型推断

通常,TypeScript类型推断是“自下而上”的:从表达式树的叶子到其根。 在下面的示例中,TypeScript通过在返回表达式中自底向上流动类型信息来推断“ number”作为函数“ mul”的返回类型。

function mul(a: number, b: number) {  
    return a * b;  
}

对于没有类型注释或默认值的变量和参数,TypeScript推断类型为“ any”,以确保编译器不需要有关函数调用位置的非本地信息即可推断函数的返回类型。 通常,这种自下而上的方法为程序员提供了有关类型信息流的清晰直觉。

但是,在某些有限的上下文中,推断是从表达式的上下文“自顶向下”进行的。 发生这种情况的地方称为上下文类型化。 当程序员使用一种类型但可能不知道该类型的所有详细信息时,上下文类型化帮助工具提供了出色的信息。 例如,在上面的jQuery示例中,程序员将函数表达式作为第二个参数提供给’get’方法。 在键入该表达式期间,工具可以假定函数表达式的类型与“ get”签名中给出的类型相同,并且可以提供包含参数名称和类型的模板。

$.get("http://mysite.org/divContent",  
      function (data) {  
          $("div").text(data);  // TypeScript infers data is a string  
      }  
);

上下文类型对于写出对象文字也很有用。 当程序员键入对象文字时,上下文类型提供的信息使工具能够为对象成员名称提供补全。

第4.23节提供了有关上下文类型表达式的更多信息。

1.6 类

JavaScript实践有两种非常常见的设计模式:模块模式和类模式。粗略地说,模块模式使用闭包来隐藏名称并封装私有数据,而类模式使用原型链来实现面向对象继承机制的许多变体。诸如“ prototype.js”之类的库是这种做法的典型。 TypeScript的命名空间是模块模式的形式化形式。 (现在,术语“模块模式”有点不幸,因为ECMAScript 2015以与模块模式规定的方式不同的形式正式支持模块。因此,TypeScript使用术语“命名空间”来表示其模块模式的形式化。)

本节和下面的命名空间部分将展示TypeScript在为类和命名空间生成ECMAScript 3或5兼容代码时如何生成一致的惯用JavaScript。 TypeScript转换的目标是确切地传达程序员在实现类或工具所没有的命名空间时键入的内容。本节还将描述TypeScript如何为每个类声明推断类型。我们将从一个简单的BankAccount类开始。

class BankAccount {  
    balance = 0;  
    deposit(credit: number) {  
        this.balance += credit;  
        return this.balance;  
    }  
}  

此类生成以下JavaScript代码。

var BankAccount = (function () {  
    function BankAccount() {  
        this.balance = 0;  
    }  
    BankAccount.prototype.deposit = function(credit) {  
        this.balance += credit;  
        return this.balance;  
    };  
    return BankAccount;  
})();

此TypeScript类声明创建一个名为“ BankAccount”的变量,其值是“ BankAccount”实例的构造函数。 此声明还会创建一个具有相同名称的实例类型。 如果我们将这种类型编写为接口,则如下所示。

interface BankAccount {  
    balance: number;  
    deposit(credit: number): number;  
}

如果我们要为“ BankAccount”构造函数变量写出函数类型声明,它将具有以下形式。

var BankAccount: new() => BankAccount;

函数签名以关键字“ new”为前缀,指示必须将“ BankAccount”函数作为构造函数调用。 函数的类型可能同时具有调用和构造函数签名。 例如,内置的JavaScript Date对象的类型包括两种签名。

如果要使用初始余额启动银行帐户,则可以向“ BankAccount”类添加构造函数声明。

class BankAccount {  
    balance: number;  
    constructor(initially: number) {  
        this.balance = initially;  
    }  
    deposit(credit: number) {  
        this.balance += credit;  
        return this.balance;  
    }  
}

此版本的’BankAccount’类要求我们引入一个构造函数参数,然后将其分配给’balance’字段。 为了简化这种常见情况,TypeScript接受以下简略语法。

class BankAccount {  
    constructor(public balance: number) {  
    }  
    deposit(credit: number) {  
        this.balance += credit;  
        return this.balance;  
    }  
}

“ public”关键字表示构造函数参数将保留为字段。 public是类成员的默认可访问性,但是程序员也可以为类成员指定private或protected的可访问性。 可访问性是设计时的构造; 它在静态类型检查期间强制执行,但并不意味着任何运行时强制执行。

TypeScript类还支持继承,如以下示例所示。* *

class CheckingAccount extends BankAccount {  
    constructor(balance: number) {  
        super(balance);  
    }  
    writeCheck(debit: number) {  
        this.balance -= debit;  
    }  
}

在此示例中,类“ CheckingAccount”派生自类“ BankAccount”。 “ CheckingAccount”的构造函数使用“ super”关键字调用类“ BankAccount”的构造函数。 在生成的JavaScript代码中,“ CheckingAccount”的原型将链接到“ BankAccount”的原型。

TypeScript类也可以指定静态成员。 静态类成员成为类构造函数的属性。

第8节提供了有关类的其他信息。

1.7 枚举类型

使用TypeScript,程序员可以将一组数字常量总结为一个枚举类型。 下面的示例创建一个枚举类型来表示计算器应用程序中的运算符。

const enum Operator {  
    ADD,  
    DIV,  
    MUL,  
    SUB  
}

function compute(op: Operator, a: number, b: number) {  
    console.log("the operator is" + Operator[op]);  
    // ...  
}

在此示例中,计算功能使用枚举类型的功能记录运算符“ op”:从枚举值(op)到与该值对应的字符串的反向映射。 例如,“运算符”的声明会自动将从零开始的整数分配给列出的枚举成员。 第9节介绍了程序员还可以如何将整数显式分配给枚举成员,以及如何使用任何字符串来命名枚举成员。

当使用const修饰符声明枚举时,TypeScript编译器将为该枚举成员生成与该成员的分配值相对应的JavaScript常量(带有注释)。 这样可以提高许多JavaScript引擎的性能。

例如,“计算”功能可能包含如下的switch语句。

switch (op) {  
    case Operator.ADD:  
        // execute add  
        break;  
    case Operator.DIV:  
        // execute div  
        break;  
    // ...  
}

对于此switch语句,编译器将生成以下代码。

switch (op) {  
    case 0 /* Operator.ADD */:  
        // execute add  
        break;  
    case 1 /* Operator.DIV */:  
        // execute div  
        break;  
    // ...  
}

JavaScript实现可以使用这些显式常量为该switch语句生成有效的代码,例如,通过构建一个由case值索引的跳转表。

1.8 字符串参数重载

TypeScript的一个重要目标是为现有的JavaScript编程模式提供准确而直接的类型。 为此,TypeScript包括将在下一节中讨论的泛型类型,以及本节的主题字符串参数的重载。

JavaScript编程接口通常包含一些函数,这些函数的行为可以通过传递给该函数的字符串常量来区分。 文档对象模型大量使用了这种模式。 例如,下面的屏幕快照显示’document’对象的’createElement’方法具有多个签名,其中一些签名标识了将特定字符串传递给该方法时返回的类型。
在这里插入图片描述
以下代码片段使用此功能。 因为推断’span’变量的类型为’HTMLSpanElement’,所以代码可以无静态错误地引用’span’的’isMultiline’属性。

var span = document.createElement("span");  
span.isMultiLine = false;  // OK: HTMLSpanElement has isMultiline property

在下面的屏幕快照中,一个编程工具将字符串参数重载的信息与上下文类型相结合,以推断变量’e’的类型为’MouseEvent’,因此’e’具有’clientX’属性。
在这里插入图片描述
第3.9.2.4节节提供了有关如何在函数签名中使用字符串文字的详细信息。

1.9 通用类型和功能

类似于字符串参数的重载,泛型类型使TypeScript更容易准确地捕获JavaScript库的行为。 因为它们使类型信息能够从客户端代码,库代码再流回到客户端代码,所以泛型类型可以比任何其它TypeScript功能做更多的事情来支持详细的API描述。

为了说明这一点,让我们看一下内置JavaScript数组类型的TypeScript接口的一部分。 您可以在TypeScript发行版随附的’lib.d.ts’文件中找到此接口。

interface Array<T> {  
    reverse(): T[];  
    sort(compareFn?: (a: T, b: T) => number): T[];  
    // ...   
}

与上面的定义类似,接口定义可以具有一个或多个类型参数。 在这种情况下,“数组”接口只有一个参数“T”,用于定义数组的元素类型。 ‘reverse’方法返回具有相同元素类型的数组。 sort方法采用一个可选参数’compareFn’,其类型是一个函数,该函数采用两个类型为’T’的参数并返回一个数字。 最后,sort返回元素类型为“T”的数组。

函数也可以具有通用参数。 例如,数组接口包含一个“map”方法,定义如下:

map<U>(func: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];

在元素类型为“ T”的数组“ a”上调用的map方法将对每个“ a”元素应用函数“ func”,并返回类型为“ U”的值。

TypeScript编译器通常可以推断通用方法参数,从而使程序员无需显式提供它们。 在下面的示例中,编译器推断map方法的参数’U’具有类型’string’,因为传递给map的函数返回一个字符串。

function numberToString(a: number[]) {  
    var stringArray = a.map(v => v.toString());  
    return stringArray;  
}

在此示例中,编译器推断’numberToString’函数返回字符串数组。

在TypeScript中,类也可以具有类型参数。 以下代码声明一个类,该类实现“T”类型的项目的链接列表。 此代码说明程序员如何限制类型参数以扩展特定类型。 在这种情况下,列表上的项目必须扩展“ NamedItem”类型。 这使程序员可以实现“日志”功能,该功能可以记录项目的名称。

interface NamedItem {  
    name: string;  
}

class List<T extends NamedItem> {  
    next: List<T> = null;

    constructor(public item: T) {  
    }

    insertAfter(item: T) {  
        var temp = this.next;  
        this.next = new List(item);  
        this.next.next = temp;  
    }

    log() {  
        console.log(this.item.name);  
    }

    // ...  
}

第3.7节提供了有关泛型类型的更多信息。

1.10 命名空间

类和接口通过提供一种机制来描述如何使用可以与该组件的实现分离的软件组件,从而支持大规模JavaScript开发。 TypeScript在设计时(通过限制使用私有成员和受保护成员)在类中实施实现的封装,但由于在运行时可以访问所有对象属性,因此无法在运行时实施封装。 JavaScript的未来版本可能会提供private名称,这将使private成员和protected成员的运行时实施成为可能。

在JavaScript中,在运行时强制执行封装的一种非常常见的方法是使用模块模式:使用闭包变量封装私有字段和方法。模块模式是通过在软件组件周围绘制边界来提供组织结构和动态加载选项的自然方法。模块模式还可以提供引入命名空间的能力,从而避免对大多数软件组件使用全局命名空间。

以下示例说明了JavaScript模块模式。

(function(exports) {  
    var key = generateSecretKey();  
    function sendMessage(message) {  
        sendSecureMessage(message, key);  
    }  
    exports.sendMessage = sendMessage;  
})(MessageModule);

此示例说明了模块模式的两个基本元素:模块关闭和模块对象。模块关闭是封装模块实现的函数,在这种情况下,变量为“ key”,函数为“ sendMessage”。模块对象包含导出的变量和模块功能。简单的模块可以创建并返回模块对象。上面的模块将模块对象作为参数“ exports”,并将“ sendMessage”属性添加到模块对象。这种扩充方法简化了模块的动态加载,还支持将模块代码分成多个文件。

该示例假定外部词汇范围定义了函数’generateSecretKey’和’sendSecureMessage’;它还假定外部作用域已将模块对象分配给变量“ MessageModule”。

TypeScrip的命名空间提供了一种简洁表达模块模式的机制。在TypeScript中,程序员可以通过在外部命名空间中嵌套命名空间和类来将模块模式与类模式组合。

以下示例显示了简单命名空间的定义和使用。

namespace M {  
    var s = "hello";  
    export function f() {  
        return s;  
    }  
}

M.f();  
M.s;  // Error, s is not exported

在此示例中,变量“ s”是命名空间的私有功能,但是函数“ f”是从命名空间导出的,并且可以在命名空间之外的代码中访问。 如果要用接口和变量来描述命名空间“ M”的作用,我们将写

interface M {  
    f(): string;  
}

var M: M;

接口“ M”概括了命名空间“ M”的外部可见行为。 在此示例中,我们可以为接口使用与初始化变量相同的名称,因为在TypeScript中,类型名和变量名不会冲突:每个词法作用域都包含变量声明空间和类型声明空间(有关更多详细信息,请参见第2.3节)。

TypeScript编译器为命名空间生成以下JavaScript代码:

var M;  
(function(M) {  
    var s = "hello";  
    function f() {  
        return s;  
    }  
    M.f = f;  
})(M || (M = {}));

在这种情况下,编译器假定命名空间对象位于全局变量“ M”中,该变量可能已初始化或尚未初始化为所需的命名空间对象。

1.11 模块

TypeScript还支持ECMAScript 2015模块,这些模块是包含顶级导出和导入指令的文件。 对于这种类型的模块,TypeScript编译器可以生成ECMAScript 2015兼容代码以及下级ECMAScript 3或5兼容代码,以用于各种模块加载系统,包括CommonJS,异步模块定义(AMD)和通用模块定义(UMD) 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章