TypeScript入门

一、什么是 TypeScript?

  • 百度解释:
    TypeScript是一种由微软开发的自由和 开源 的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的 面向对象编程 。它扩展了 JavaScript 的语法,所以任何现有的JavaScript程序可以不加改变的在TypeScript下工作。TypeScript是为大型应用之开发而设计,而编译时它产生 JavaScript 以确保兼容性
  • 通俗解释:
    TypeScript充分利用了JavaScript原有的对象模型并在此基础上做了扩充,添加了较为严格的 类型检查机制 ,添加了 模块支持和API导出 的能力。比起JavaScript,TypeScript提供了更多在语言层面上的支持,使得程序员能够以更加标准化的语法来表达语义上的约束,从而降低了程序出错的机率;TypeScript也使得代码组织和复用变得更加有序,使得开发大型Web应用有了一套标准方法。
  • 二、TypeScript优势

  • 支持ES6(TypeScript脚本语言的语法会成为未来一段时间客户端语言的主流语法)
  • 强大的IDE支持
  • Angular2框架的开发语言(学习Typescript语言帮你更好的帮你去学习Angular2的框架)
  • 强大的IDE支持体现在三个特性上:

    1.类型检测
    在Typescript里面是运行为变量指定类型的,比如当你为这个变量指定数字类型的值的时候,IDE会做出类型检查,然后告诉你这里可能会有错误,这个特性会减少你在开发阶段犯错误的几率。
    2.语法提示
    在IDE里面去编写TypeScript的代码时,IDE会根据你当前的上下文,把你能用的类、变量、方法和关键字都给你提示出来,你只要直接去选就可以了,这个特性会大大提升你的开发效率。
    重构是说你可以很方便的去修改你的变量或者方法的名字或者是文件的名字,当你做出这些修改的时候,IDE会帮你自动引用这个变量或者调用这个方法地方的代码自动帮你修改掉,这个特性一个是会提高你的开发效率,另一个是可以很容易的提升你的代码质量。

    三、TypeScript缺点

  • 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的东西。而且它的中文资料也不多。
  • 短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,TypeScript 能够减少其维护成本。
  • 集成到构建流程需要一些工作量。
  • 可能和一些库结合的不是很完美。
  • 四、TypeScript基础语法

    1.基础类型

    布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean 定义布尔值类型

    let isDone: boolean = false; 
    

    和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number,除了支持十进制和十六进制字面量,还支持ECMAScript 2015中引入的二进制和八进制字面量。

    let decLiteral: number = 6;  //十进制
    let hexLiteral: number = 0xf00d;   //十六进制
    let binaryLiteral: number = 0b1010; // ES6 中的二进制表示法
    let octalLiteral: number = 0o744;  // ES6 中的八进制表示法
    
    var decLiteral = 6;
    var hexLiteral = 0xf00d;
    var binaryLiteral = 10;
    var octalLiteral = 484;
    

    其中 0b10100o744ES6 中的二进制和八进制表示法,它们会被编译为十进制数字。

    使用 string 定义字符串类型,其中用 ` 来定义 ES6 中的模板字符串,${expr}用来在模板字符串中嵌入表达式。

    let myName: string = 'Wave';
    let myAge: number = 23;
    // 模板字符串
    let sentence: string = `Hello, my name is ${myName}.
    I'll be ${myAge + 1} years old next month.`;
    
    var myName = 'Wave';
    var myAge = 23;
    // 模板字符串
    var sentence = "Hello, my name is " + myName + ".\nI'll be " + (myAge + 1) + " years old next month.";
    
    Void(空值)

    JavaScript 没有空值(Void)的概念,在 TypeScirpt 中,可以用 void 表示没有任何返回值的函数。

    function warnUser(): void {
        alert("This is my warning message");
    

    声明一个void类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null:

    let unusable: void = undefined;
    
    Null 和 Undefined

    TypeScript里,undefined和null两者各自有自己的类型分别叫做undefinednull
    undefined 类型的变量只能被赋值为 undefined,null 类型的变量只能被赋值为 null

    let u: undefined = undefined;
    let n: null = null;
    

    与 void 的区别是,undefinednull是所有类型的子类型。也就是说 undefined类型的变量,可以赋值给 number类型的变量:

    // 这样不会报错
    let u: undefined;
    let num: number = u;
    // 这样会报错
    let u: void;
    let num: number = u;
    // index.ts(2,5): error TS2322: Type 'void' is not assignable to type 'number'.
    

    注意:我们鼓励尽可能地使用--strictNullChecks,因为指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免很多常见的问题。

    Any 任意值

    一个普通类型,在赋值过程中改变类型是不被允许的,而any 类型,则允许被赋值为任意类型。

    //这样会报错
    let myFavoriteNumber: string = 'seven';
    myFavoriteNumber = 7;
    //这样不会报错
    let myFavoriteNumber: any = 'seven';
    myFavoriteNumber = 7;
    
  • 声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
  • 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型。
  • Never

    never类型表示的是那些永不存在的值的类型。 例如, neverr类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。
    never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
        throw new Error(message);
    // 推断的返回值类型为never
    function fail() {
        return error("Something failed");
    // 返回never的函数必须存在无法达到的终点
    function infiniteLoop(): never {
        while (true) {
    

    联合类型(Union Types)表示取值可以为多种类型中的一种。联合类型使用 | 分隔每个类型

    let myFavoriteNumber: string | number;
    myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;
    

    当 TypeScript 不确定一个联合类型的变量到底是哪个类型时,我们只能访问此联合类型的所有类型里共有的属性或方法

    //这样会报错
    function getLength(something: string | number): number {
        return something.length;
    //这样不会报错
    function getString(something: string | number): string {
        return something.toString();
    

    上例中length 不是 string 和 number 的共有属性,所以会报错。访问 string 和 number 的共有属性是没问题的。

    联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

    let myFavoriteNumber: string | number;
    myFavoriteNumber = 'seven';
    console.log(myFavoriteNumber.length); // 5
    myFavoriteNumber = 7;
    console.log(myFavoriteNumber.length); // 编译时报错
    

    上例中,第二行的 myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错。
    而第四行的 myFavoriteNumber被推断成了 number,访问它的 length 属性时就报错了。

    对象的类型——接口

    在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型

    interface Person {
        name: string;
        age: number;
    let tom: Person = {
        name: 'Tom',
        age: 25
    

    我们定义了一个接口 Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。
    定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致

    interface Person {
        name: string;
        age: number;
    //会编译错误
    let tom: Person = {
        name: 'Tom'
    //会编译错误
    let tom: Person = {
        name: 'Tom',
        age: 25,
        gender: 'male'
    

    有时我们希望不要完全匹配一个形状,那么可以用可选属性

    interface Person {
       name: string;
       age?: number;
    let tom: Person = {
       name: 'Tom'
    

    可选属性属性的含义是该属性可以不存在,但仍然不允许添加未定义的属性

    //编译会报错
    interface Person {
        name: string;
        age?: number;
    let tom: Person = {
        name: 'Tom',
        age: 25,
        gender: 'male'
    

    有时候我们希望一个接口允许有任意属性,可以使用如下方式:

    interface Person {
        name: string;
        age?: number;
        [propName: string]: any;
    let tom: Person = {
        name: 'Tom',
        gender: 'male'
    

    有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

    interface Person {
        readonly id: number;
        name: string;
        age?: number;
        [propName: string]: any;
    let tom: Person = {
        id: 89757,
        name: 'Tom',
        gender: 'male'
    tom.id = 9527; //会报错
    

    使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。

    在 TypeScript 中,有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:

    let list: number[] = [1, 2, 3];
    

    第二种方式是使用数组泛型,Array<元素类型>

    let list: Array<number> = [1, 2, 3];
    
    函数的类型

    在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

    // 函数声明(Function Declaration)
    function sum(x, y) {
        return x + y;
    // 函数表达式(Function Expression)
    let mySum = function (x, y) {
        return x + y;
    

    一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

    function sum(x: number, y: number): number {
        return x + y;
    

    注意,输入多余的(或者少于要求的)参数,是不被允许的

    function sum(x: number, y: number): number {
        return x + y;
    sum(1, 2, 3);
    // index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
    function sum(x: number, y: number): number {
        return x + y;
    sum(1);
    // index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
    
  • 函数表达式
  • 如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:

    let mySum = function (x: number, y: number): number {
        return x + y;
    

    这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样:

    let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
        return x + y;
    

    注意:不要混淆了 TypeScript 中的 => 和 ES6 中的 =>
    在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
    在 ES6 中,=> 叫做箭头函数,应用十分广泛,可以参考 ES6 中的箭头函数

  • 用接口定义函数的形状
  • 我们也可以使用接口的方式来定义一个函数需要符合的形状:

    interface SearchFunc {
        (source: string, subString: string): boolean;
    let mySearch: SearchFunc;
    mySearch = function(source: string, subString: string) {
        return source.search(subString) !== -1;
    

    前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?
    与接口中的可选属性类似,我们用 ? 表示可选的参数:

    function buildName(firstName: string, lastName?: string) {
        if (lastName) {
            return firstName + ' ' + lastName;
        } else {
            return firstName;
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName('Tom');
    

    需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必须参数了

    function buildName(firstName?: string, lastName: string) {
        if (firstName) {
            return firstName + ' ' + lastName;
        } else {
            return lastName;
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName(undefined, 'Tom');
    // index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.
    
  • 参数默认值
  • 在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

    function buildName(firstName: string, lastName: string = 'Cat') {
        return firstName + ' ' + lastName;
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName('Tom');
    

    此时就不受「可选参数必须接在必需参数后面」的限制了:

    function buildName(firstName: string = 'Tom', lastName: string) {
        return firstName + ' ' + lastName;
    let tomcat = buildName('Tom', 'Cat');
    let cat = buildName(undefined, 'Cat');
    

    关于默认参数,可以参考 ES6 中函数参数的默认值

    ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数):

    function push(array, ...items) {
        items.forEach(function(item) {
            array.push(item);
    let a = [];
    push(a, 1, 2, 3);
    

    事实上,items 是一个数组。所以我们可以用数组的类型来定义它:

    function push(array: any[], ...items: any[]) {
        items.forEach(function(item) {
            array.push(item);
    let a = [];
    push(a, 1, 2, 3);
    

    注意,rest 参数只能是最后一个参数,关于 rest 参数,可以参考 ES6 中的 rest 参数

    重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
    比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。
    利用联合类型,我们可以这么实现:

    function reverse(x: number | string): number | string {
        if (typeof x === 'number') {
            return Number(x.toString().split('').reverse().join(''));
        } else if (typeof x === 'string') {
            return x.split('').reverse().join('');
    

    然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
    这时,我们可以使用重载定义多个 reverse 的函数类型:

    function reverse(x: number): number;
    function reverse(x: string): string;
    function reverse(x: number | string): number | string {
        if (typeof x === 'number') {
            return Number(x.toString().split('').reverse().join(''));
        } else if (typeof x === 'string') {
            return x.split('').reverse().join('');
    

    上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
    注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

    2.类型推论

    如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

    //虽然没有指定类型,但是会在编译的时候报错
    let myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;
    // index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
    //等价于以下代码
    let myFavoriteNumber: string = 'seven';
    myFavoriteNumber = 7;
    

    如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:

    let myFavoriteNumber;
    myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;
    

    3.类型断言

    类型断言(Type Assertion)可以用来手动指定一个值的类型。
    语法<类型>值 或者 值 as 类型

    例子:将一个联合类型的变量指定为一个更加具体的类型
    当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

    function getLength(something: string | number): number {
        return something.length;
    // index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
    //   Property 'length' does not exist on type 'number'.
    

    而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,比如:

    function getLength(something: string | number): number {
        if (something.length) {
            return something.length;
        } else {
            return something.toString().length;
    // index.ts(2,19): error TS2339: Property 'length' does not exist on type 'string | number'.
    //   Property 'length' does not exist on type 'number'.
    // index.ts(3,26): error TS2339: Property 'length' does not exist on type 'string | number'.
    //   Property 'length' does not exist on type 'number'.
    

    上例中,获取something.length的时候会报错。
    此时可以使用类型断言,将something 断言成 string

    function getLength(something: string | number): number {
        if ((<string>something).length) {
            return (<string>something).length;
        } else {
            return something.toString().length;
    

    类型断言的用法如上,在需要断言的变量前加上<Type>即可
    类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的:

    function toBoolean(something: string | number): boolean {
        return <boolean>something;
    // index.ts(2,10): error TS2352: Type 'string | number' cannot be converted to type 'boolean'.
    //   Type 'number' is not comparable to type 'boolean'.
    

    4.声明文件

    当使用第三方库时,我们需要引用它的声明文件。
    假如我们想使用第三方库,比如 jQuery,我们通常这样获取一个 id 是 foo 的元素:

    $('#foo');
    // or
    jQuery('#foo');
    

    但是在 TypeScript 中,我们并不知道 $ 或 jQuery 是什么东西:

    jQuery('#foo');
    // index.ts(1,1): error TS2304: Cannot find name 'jQuery'.
    

    这时,我们需要使用 declare 关键字来定义它的类型,帮助 TypeScript 判断我们传入的参数类型对不对:

    declare var jQuery: (selector: string) => any;
    jQuery('#foo');
    

    declare 定义的类型只会用于编译时的检查,编译结果中会被删除。上例的编译结果是:

    jQuery('#foo');
    通常我们会把类型声明放到一个单独的文件中,这就是声明文件:

    // jQuery.d.ts
    declare var jQuery: (string) => any;
    

    我们约定声明文件以 .d.ts 为后缀。
    然后在使用到的文件的开头,用「三斜线指令」表示引用了声明文件:

    /// <reference path="./jQuery.d.ts" />
    jQuery('#foo');
    

    第三方声明文件
    当然,jQuery 的声明文件不需要我们定义了,已经有人帮我们定义好了:jQuery in DefinitelyTyped。我们可以直接下载下来使用,但是更推荐的是使用工具统一管理第三方库的声明文件。 TypeScript 2.0 推荐使用 @types 来管理
    @types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

    npm install @types/jquery --save-dev
    

    5.内置对象

    JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。
    内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。
    ECMAScript 的内置对象
    ECMAScript 标准提供的内置对象有:BooleanErrorDateRegExp等。
    我们可以在 TypeScript 中将变量定义为这些类型:

    let b: Boolean = new Boolean(1);
    let e: Error = new Error('Error occurred');
    let d: Date = new Date();
    let r: RegExp = /[a-z]/;
    

    更多的内置对象,可以查看 MDN 的文档
    而他们的定义文件,则在 TypeScript 核心库的定义文件中。
    DOM 和 BOM 的内置对象
    DOM 和 BOM 提供的内置对象有:DocumentHTMLElementEventNodeList等。
    TypeScript 中会经常用到这些类型:

    let body: HTMLElement = document.body;
    let allDiv: NodeList = document.querySelectorAll('div');
    document.addEventListener('click', function(e: MouseEvent) {
      // Do something
    

    它们的定义文件同样在 TypeScript 核心库的定义文件中。

    TypeScript 核心库的定义文件
    TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。
    当你在使用一些常用的方法的时候,TypeScript 实际上已经帮你做了很多类型判断的工作了,比如:

    Math.pow(10, '2');
    //index.ts(1,14): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
    

    上面的例子中,Math.pow 必须接受两个 number类型的参数。事实上 Math.pow的类型定义如下:

    interface Math {
         * Returns the value of a base expression taken to a specified power.
         * @param x The base value of the expression.
         * @param y The exponent value of the expression.
        pow(x: number, y: number): number;
    

    再举一个 DOM 中的例子:

    document.addEventListener('click', function(e) {
        console.log(e.targetCurrent);
    //index.ts(2,17): error TS2339: Property 'targetCurrent' does not exist on type 'MouseEvent'.
    

    上面的例子中,addEventListener方法是在 TypeScript 核心库中定义的:

    interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
        addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
    

    所以 e 被推断成了 MouseEvent,而MouseEvent是没有 targetCurrent 属性的,所以报错了。