【TypeScript入门教程】个人学习笔记

TypeScript入门教程

一、简介

1.什么是TypeScript

官网对TypeScript的定义:
Typed JavaScript at Any Scale.
添加了类型系统的JavaScript,适用于任何规模的项目。

1.1 TypeScript的特性

JavaScrip的特性:

  • 没有类型约束,一个变量可能初始化是字符串,过一会儿又被赋值为数字
  • 由于隐式类型转换的存在,有的变量的类型很难在运行前就确定
  • 基于原型的面向对象的编程,使得原型上的属性或方法可以在运行时被修改
  • 函数是JavaScript中的一等公民,可以赋值给变量,也可以当作参数或返回值
    JavaScrip的灵活性就像一把双刃剑,另一方面也使得它的代码质量参差不齐,维护成本高,运行时错误多。

TypeScript 的类型系统,在很大程度上弥补了 JavaScript 的缺点。

1.1.1 类型系统

类型系统按照【类型检查的时机】来分类,可以分为动态类型和静态类型:

  • 动态类型:是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误
  • 静态类型:是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误

类型系统按照【是否允许隐士转换】来分类,可以分为强类型和弱类型。

  • 强类型:Python;弱类型:JavaScript
  • 强/弱是相对的,Python在处理整型和浮点型相加时,会将整型隐式转换为浮点型,但这并不影响Python是强类型的结论,因为大部分情况下Pyhon并不会进行隐式类型转换。

1.1.2 TypeScript的特性

  • TypeScrip是静态类型
  • TypeScript是弱类型
  • 适用于任何规模
TypeScript是静态类型
  • JavaScript是一门解释型语音,没有编译阶段,所以它是动态类型
    • 以下代码在运行时才会报错:
    let foo = 1;
    foo.split(' ');
    // Uncaught TypeError: foo.split is not a function
    // 运行时会报错(foo.split 不是一个函数),造成线上 bug`
    
  • TypeScrip在运行前需要先编译为JavaScript,而在编译阶段就会进行类型检查,所以,TypeScript是静态类型
    • 以下代码在编译阶段就会报错:
    let foo = 1;
    foo.split(' ');
    // Property 'split' does not exist on type 'number'.
    // 编译时会报错(数字没有 split 方法),无法通过编译
    
TypeScript是弱类型
  • TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型
    • 以下代码在JavaScrit和TypeScript中都可以正常运行,运行时数字1会被隐式类型转换为字符串'1',加号+被识别为字符串拼接,打印结果是字符串'11'
      console.log(1 + '1') // 打印出字符串 '11'
  • Python是强类型
    • 以下代码会在运行时报错
      print(1 + '1')        # TypeError: unsupported operand type(s) for +: 'int' and 'str'
      
    • 如果要修复该错误,需要进行强制类型转换:
    print(str(1) + '1')        # 打印出字符串 '11'
    

TypeScript的类型系统体现了它的核心设计理念:在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug

适用于任何规模
  • 一些第三方库原生支持了 TypeScript,在使用时就能获得代码补全了,比如 Vue 3.0
  • 有一些第三方库原生不支持 TypeScript,但是可以通过安装社区维护的类型声明库(比如通过运行 npm install --save-dev @types/react 来安装 React 的类型声明库

2. 安装TypeScript

命令行工具安装TypeScript:

  • npm install -g typescript
  • 以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了

编译一个TypeScript文件:

  • tsc hello.ts
  • 我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀

编辑器:

  • 主流的编辑器都支持 TypeScript,推荐使用 Visual Studio Code
    • Visual Studio Code本身也是用 TypeScript 编写的

二、基础

1. 原始数据类型

JavaScript的类型分为两种:

  • 原始数据类型
    • 原始数据类型包括:布尔值、数值、字符串、nullundefined以及ES6中的新类型symbol和ES10中的新类型Bright
  • 对象类型。

1.1 布尔值

返回的是布尔值:

  • 布尔值是最基础的数据类型,在TypeScript中,使用boolean定义布尔值类型:
    let isDone: boolean = false;
    
  • 直接调用Boolean也可以返回一个boolean类型:
    let createdByBoolean: boolean = Boolean(1);
    

返回的是一个对象(包装对象):

  • 注意,使用构造函数Boolean创造的对象不是布尔值:
    let createdByNewBoolean: boolean = new Boolean(1);
    
    // Type 'Boolean' is not assignable to type 'boolean'.
    //   'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.
    
  • new Boolean返回的是一个Boolean对象:
    let createdByNewBoolean: Boolean = new Boolean(1);
    

在TypeScript中,boolean是JavaScript中的基本类型,而Boolean是JavaScript中的构造函数。其他基本类型(除了nullundefined)一样,不再赘述。

1.2 数值

使用number定义数值类型:

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

编译结果:

var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;

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

1.3 字符串

使用string定义字符串类型:
添加链接描述

let myName: string = 'Jerry';
let myAge: number = 15;
//模板字符串
let sentence:string = `hello,this is ${myName}.I'll be ${myAge + 1} years old next month.`;

编译结果:

var myName = 'Jerry';
var myAge = 15;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".
I'll be " + (myAge + 1) + " years old next month.";

其中, 反引号 用来定义ES6中的模板字符串,${expr}用来在模板字符串中嵌入表达式。

1.4 空值

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

function alertName(): void {
    alert('My name is Tom');
}

声明一个void类型的变量没有什么用,因为你只能将他赋值给undefinednull(只在 strictNullChecks——TypeScript中严格的空校验 未指定时):

let unusable: void = undefined;

1.5 Null和Undefined

在TypeScript中,可以使用nullundefined来定义这两个原始数据类型:

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

void的区别:undefinednull是所有类型的子类型,可以把undefinednull赋值给其他类型的变量

也就是说,undefined类型的变量,可以赋值给number类型的变量:

// 这样不会报错
let num: number = undefined;    //非严格模式
let num: number = undefined; // 严格模式下,会报错: Type 'undefined' is not assignable to type 'number'

// 这样也不会报错
let u: undefined;    //非严格模式
let num: number = u;    //非严格模式

/**
	非严格模式下,变量的值可以为 undefined 或 null
	而严格模式下,变量的值只能为 undefined
**/

void类型的变量不能赋值给number类型的变量:

let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.

2. 任意值

任意值(any)用来表示允许赋值为任意类型。

2.1 什么是任意值类型

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

let myFavoriteNumber: string = 'Jerry';
myFavoriteNumber = 8;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但如果是any类型,则允许被赋值为任意类型:

let myFavoriteNumber: any = 'Jerry';
myFavoriteNumber = 8;

2.2 任意值的属性和方法

在任意值上访问任何属性都是允许的:

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

也允许调用任何方法:

let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值

2.3 未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

let something;
something = 'Jerry';
something = 7;

something.setName('Tom');

等价于:

let something: any;
something = 'Jerry';
something = 7;

something.setName('Tom');

3. 类型推论

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

3.1 什么是类型推论

TypeScript会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

  • 以下代码虽然没有指定类型,但会在编译的时候报错:
    TS会自动推测出myFavoriteNumber的类型为string,所以给它赋值为number会报错

    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;
    
    // index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
    
  • 如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成any类型而完全不被类型检查。

    let myFavoriteNumber;    //等价于 let myFavoriteNumber : any ;
    myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;
    

4. 联合类型

联合类型(Union Type)表示取值可以为多种类型中的一种。

4.1 一个简单的例子

let myFavoriteNumber: string | number;
myFavoriteNumber = 'Jerry';
myFavoriteNumber = 8;

myFavoriteNumber = true;

// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
//   Type 'boolean' is not assignable to type 'number'.

/**
   let myFavoriteNumber: string | number 的含义是,允许 myFavoriteNumber 的类型是 string 或者 number,但是不能是其他类型
**/

联合类型使用|分隔每个类型

4.2 访问联合类型的属性或方法

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

  • 如下代码,length 不是 stringnumber 的共有属性,所以会报错
    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'.
    
  • 访问 stringnumber 的共有属性是没问题的
    function getString(something: string | number): string {
        return something.toString();
    }
    

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

let myFavoriteNumber: string | number;

myFavoriteNumber = 'seven';
//myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错
console.log(myFavoriteNumber.length); // 5

myFavoriteNumber = 7;
// myFavoriteNumber 被推断成了 number,访问它的 length 属性会报错
console.log(myFavoriteNumber.length); // 编译时报错

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

5. 对象的类型——接口

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

5.1 什么是接口

  • 在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动由类(classes)去实现(implement)。
  • 接口一般首字母大写,有的编程语言中会建议接口的名称加上I前缀。

5.2 一个简单的例子

  • 这是一个简单的例子

    //定义一个接口Person
    interface Person {
        name: string;
        age: number;
    }
    
    /**
      定义一个变量tom,它的类型是Person
      约定tom的形状必须和接口Person一致
    **/
    let tom: Person = {
        name: 'Tom',
        age: 25
    };
    
  • 定义的变量比接口少/多一些属性是不允许的:

    interface Person {
        name: string;
        age: number;
    }
    
    /**
      变量tom比接口Person少一个age属性,这是不允许的
    **/
    let tom: Person = {
        name: 'Tom'
    };
    
    /**
    报错:
    	index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
    	Property 'age' is missing in type '{ name: string; }'.
    **/ 
    
    // 变量jerry比接口多一个gender属性,这是不允许的
    let jerry: Person = {
        name: 'Tom',
        age: 25,
        gender: 'male'
    };
    
    /**
    报错:
    	index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
    	Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
    **/
    

可见,赋值的时候,变量的形状必须和接口的形状保持一致!

5.3 可选属性(?)

可选属性的含义是:该属性可以不存在

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

    interface Person {
        name: string;
        age?: number;
    }
    
    let tom: Person = {
        name: 'Tom'
    };
    
    let jerry: Person = {
        name: 'Tom',
        age: 25
    };
    
  • 但是,仍然不允许添加未定义的属性:

    interface Person {
        name: string;
        age?: number;
    }
    
    let tom: Person = {
        name: 'Tom',
        age: 25,
        gender: 'male'    //添加了接口Person中未定义的属性gender
    };
    /**
    报错:
    	examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
    	Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
    **/
    

5.4 任意属性

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

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;    //使用 [propName: string] 定义了任意属性取 string 类型的值
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};
  • 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是任意属性的类型的子集

    interface Person {
        name: string;
        age?: number;
        [propName: string]: string;    //任意属性的值允许string类型的属性
    }
    
    let tom: Person = {
        name: 'Tom',
        age: 25,    //age属性是number类型,number不是string的子属性,所以报错
        gender: 'male'
    };
    /**
    报错:
    	index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
    	index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
    	Index signatures are incompatible.
    	Type 'string | number' is not assignable to type 'string'.
    	Type 'number' is not assignable to type 'string'.
    **/
    

    在报错信息中可以看出,此时 { name: 'Tom', age: 25, gender: 'male' } 的类型被推断成了 { [x: string]: string | number; name: string; age: number; gender: string; },这是联合类型和接口的结合。

  • 一个接口只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

    interface Person {
        name: string;
        age?: number;
        [propName: string]: string | number;    //联合类型
    }
    
    let tom: Person = {
        name: 'Tom',
        age: 25,
        gender: 'male'
    };
    

    因为一旦定义了任意属性,确定属性和可选属性的类型必须是任意属性的子集,所以当接口有多种类型的属性时,可以将任意属性定义为联合类型。

    • 在3.9.3中,如果同时存在任意属性、可选属性,那么任意属性的数据类型要带undefined

5.5 只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以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;    //给只读属性赋值,会报错
//index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
  • 注意:只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

//第一次给对象赋值,会报错,因为没有给id属性赋值(报错1)
let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

//给只读属性赋值,会报错(报错2)
tom.id = 89757;

/**
  报错1:
	  index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
	Property 'id' is missing in type '{ name: string; gender: string; }'.
  报错2:
	  index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
**/

6. 数组的类型

在TS中,数组类型有多种定义方式,比较灵活。

6.1 [类型+方括号]表示法

最简单的方法是使用[类型+方括号]来表示数组:

//[类型+方括号]表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];

//数组的项中不允许出现其他的类型
let fibonacci2: number[] = [1, '1', 2, 3, 5];  

//数组的一些方法的参数也会根据数组在定义时约定的类型进行限制
let fibonacci3: number[] = [1, 1, 2, 3, 5];
fibonacci3.push('8');    //Argument of type '"8"' is not assignable to parameter of type 'number'.

6.2 数组泛型

我们也可以使用数组泛型(Array Generic)**Array**来表示数组

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

6.3 用接口表示数组

也可以用接口来描述数组:

interface NumberArray {
    [index: number]: number;    //任意属性
}

//NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

虽然接口也可以用来描述数组,但我们一般不会这么做,因为这种方式比前两种复杂多了。
不过有一种情况例外,那就是用它来表示类数组(伪数组)。

6.4 类(伪)数组

  • 定义
    • 拥有length属性,其他属性(索引)为非负整数(对象中的索引会被当作字符串来处理,可以当作是个非负整数串来理解)
    • 不具有数组所具有的方法
    • 伪数组,就像数组一样有length属性,也有012等属性的对象,看起来就像数组一样,但不是数组
    • 伪数组是一个Object,而真实的数组是一个Array
    • 常见的伪数组:arguments
    • 判断伪数组的方法:可以看看《javascript权威指南》,也可以用Array.isArrar()来判断

类数组(Array-like Object)不是数组类型,比如arguments

function sum() {
    let args: number[] = arguments;    //报错:Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
}
  • arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口

    function sum() {
        let args: {
            [index: number]: number;    //约束当索引的类型、值的类型必须是数字
            length: number;    //约束类数组必须存在length属性
            callee: Function;    //约束类数组存在callee属性
        } = arguments;
    }
    
  • 事实上,常用的类数组都有自己的接口定义,如IArgumentsNodeListHTMLCollection

    function sum() {
        let args: IArguments = arguments;
    }
    
    • 其中 IArguments 是 TypeScript 中定义好了的类型,它实际上就是:
      interface IArguments {
          [index: number]: any;
          length: number;
          callee: Function;
      }
      
  • 疑问(还是不理解)

    • ”一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集”,但为啥下面的代码不会报错?
    		interface IArguments {
    		    [index: number]: number;
    		    length: number;
    		    callee: Function;
    		}
    
    • 其他人的回答:
      • 任意属性的类型为string,那么确定属性和可选属性的类型都必须为它的类型的子集。
      • number 类型的任意属性签名不会影响其他 string 类型的属性签名
      • 两种任意类型签名并存时,number 类型的签名指定的值类型必须是 string 类型的签名指定的值类型的子集。
    
    /**
    	虽然指定了 number 类型的任意属性的类型是 string,
    	但 length 属性是 string 类型的签名,所以不受前者的影响。
     **/
    	type Arg = {
    	    [index: number]: string
    	    length: number
    	}
    
    
    /**
    	如果接口定义了 string 类型的任意属性签名,
    	它不仅会影响其他 string 类型的签名,也会影响其他 number 类型的签名。
    **/
    	interface Person {
        name: string;
        age?: number;
        [propName: string]: string;    //任意属性的值允许string类型的属性
    }
    

6.5 any在数组中的应用

一个比较常见的做法是:用any表示数组中允许出现任意类型:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

7. 函数的类型

一个函数有输入和输出,要在TS中对其进行约束,需要把输入和输出都考虑到。

7.1 函数定义的两种方式

在JavaScript中,有两种常见的定函数的方式:

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

7.2 函数声明的类型定义

函数声明的类型定义如下:

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

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

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

sum(1, 2, 3);    //输入多余的参数:error TS2346: Supplied parameters do not match any signature of call target.

sum(1);    //输入少于要求的参数:error TS2346: Supplied parameters do not match any signature of call target.

7.3 函数表达式的类型定义

  • 易错
    对一个函数表达式(Function Expression)类型不正确的定义:

    let mySum = function (x: number, y: number): number {
        return x + y;
    };
    
    • 如上代码,可以通过编译。不过事实上,上面的代码只对等号右边的匿名函数进行了类型定义,而等号左边的muSum,是通过赋值操作进行类型推论而推断出来的
  • 正确的写法

    //手动给mySum添加类型
    let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
        return x + y;
    };
    

注意,不要混淆了TS中的=>和ES6中的=>

  • 在TS的类型定义中,=>用来表示函数的定义,左边是输入的类型,需要用括号括起来,右边是输出的类型。
  • 在ES6中,=>是箭头函数,具体参考ES6中的箭头函数。

7.4 用接口定义函数的形状

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

interface SearchFunc {
    (source: string, subString: string): boolean;
}

//函数表达式+接口定义函数的类型
let mySearch: SearchFunc;    //对等号左边进行类型限制
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

采用函数表达式+接口定义函数类型的方式时,对等号左边进行类型限制,可以保证以后对函数名赋值时,保证参数个数、参数类型、返回值类型不变。

7.5 函数的参数

7.5.1 可选参数

?表示可选的参数。


//lastName为可选的参数
function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

注意:可选参数必须接在必选参数后面

  • 换句话说,可选参数后面不允许再出现必选参数:
    
    // 可选参数firstName应该写在最后面
    function buildName(firstName?: string, lastName: string) {
        if (firstName) {
            return firstName + ' ' + lastName;
        } else {
            return lastName;
        }
    }
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName(undefined, 'Tom');
    
    // 报错: error TS1016: A required parameter cannot follow an optional parameter.
    

7.5.2 参数默认值

在ES6中,我们允许给函数的参数添加默认值,TS会将添加了默认值的参数识别为可选参数

//将添加了默认值的参数lastName识别为可选参数
function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

添加了默认值的参数被TS识别为可选参数,但不受【可选参数必须接在必须参数后面】的限制:

//添加了默认值的参数不受【可选参数位置】的约束
function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

默认参数更多用法,参考ES6中函数参数的默认值

7.5.3 剩余参数

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

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a: any[] = [];
push(a, 1, 2, 3);

在TS中的写法:

// item一个数组,所以用数组的类型来定义它
function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

注意:rest参数只能是最后一个参数


ES6中的剩余参数详解:

  • 定义

    • ES6中引入rest参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了
    • rest参数搭配的变量是一个数组(例...val),该变量(val)将多余的参数放入数组中
  • rest参数arguments对象

    • arguments对象不是数组,而是一个类似数组的对象。为了使用数组的方法,必须使用Array.from先将其转为数组
    • rest参数是一个真正的数组,数组特有的方法都可以使用
  • rest参数和函数的length属性

    • 函数的length属性,不包括rest参数
    (function(a) {}).length  // 1
    (function(...a) {}).length  // 0
    (function(a, ...b) {}).length  // 1
    

7.6 重载

函数重载:允许一个函数接受不同数量或类型的参数时,作出不同的处理。(类似C++的重载)

  • C++函数重载:函数名相同,函数形参列表不同(函数特征标不同)的一类函数称为函数重载。
    • 注意函数重载的依据只有形参列表不同。

注意,TS 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

//函数定义
function reverse(x: number): number;
function reverse(x: string): string;

//函数实现
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

8. 类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

8.1 语法

两种声明方式:

  • 值 as 类型
    • 在tsx语法中使用
  • <类型>值
    形如的语法在tsx中表示的是一个ReactNode,在ts中除了表示类型断言外,也可能表示的是一泛型
    所以建议大家在使用类型断言时,统一使用值 as 类型的语法。

8.2 类型断言的用途

类型断言的常见用途有以下几种:

  • 将一个联合类型断言为其中一个类型
  • 将一个父类断言为更加具体的子类
  • 将任何一个类型断言为any
  • 将any断言为一个具体的类型

8.2.1 将一个联合类型断言为其中一个类型

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

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function getName(animal: Cat | Fish) {
    return animal.name;    //访问所有类型中共有的属性
}
  • 应用
    有时候,我们需要在还不确定类型时候就访问其中一个类型特有的属性或方法:

    interface Cat {
        name: string;
        run(): void;
    }
    interface Fish {
        name: string;
        swim(): void;
    }
    
    function isFish(animal: Cat | Fish) {
        if (typeof animal.swim === 'function') {    //获取animal.swim会报错
            return true;
        }
        return false;
    }
    
    /**
    报错信息:
      error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
      Property 'swim' does not exist on type 'Cat'.
    **/
    

    解决方法——使用类型断言,将animal断言为Fish

    function isFish(animal: Cat | Fish) {
        if (typeof (animal as Fish).swim === 'function') {    //将animal断言为Fish
            return true;
        }
        return false;
    }
    

注意:类型断言只能够【欺骗】TS编译器,但无法避免运行时的错误,滥用类型断言可能会导致运行时的错误:

  • 以下代码在编译时不会报错,但在运行时会报错:
    interface Cat {
        name: string;
        run(): void;
    }
    interface Fish {
        name: string;
        swim(): void;
    }
    
    function swim(animal: Cat | Fish) {
        (animal as Fish).swim();
    }
    
    const tom: Cat = {
        name: 'Tom',
        run() { console.log('run') }
    };
    swim(tom);
    // 运行时报错
    //Uncaught TypeError: animal.swim is not a function`
    
    • 原因是: (animal as Fish).swim()这段代码隐藏了animal可能为Cat的情况,将animal直接断言为Fish。TS编译器相信了我们的断言,所以在调用swim()的时候没有编译错误。但是swim函数接受的参数类型是Cat | Fish,一旦传入了Cat类型的变量,由于Cat上没有swim方法,就会导致运行时的错误了。

在使用类型断言时,一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

8.2.2 将一个父类断言为更加具体的子类

当类之间有继承关系时,类型断言也是很常见的:

  • 一个小栗子
    • 需求:定义一个函数 isApiError用来判断传入的参数是不是 ApiError 类型
    • 实现:
      • 为了实现这样一个函数,它的参数类型肯定得是比较抽象的父类 Error,这样的话这个函数就能接受 Error 或它的子类作为参数
      • 但是由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错,需要使用类型断言获取 (error as ApiError).code
        class ApiError extends Error {
            code: number = 0;
        }
        class HttpError extends Error {
            statusCode: number = 200;
        }
        
        function isApiError(error: Error) {
            if (typeof (error as ApiError).code === 'number') {
                return true;
            }
            return false;
        }
        
  • 一些小思考
    • 上面的栗子使用instanceof更加合适,因为ApiError是一个JS的类,能够通过instanceof来判断 error 是否是它的实例。

    • 但有的情况下, ApiErrorHttpError不是一个真正的类,而只是一个TS的接口,接口是一个类型,不是一个真正的值,它在编译结果中会被删除,无法使用instanceof来做运行时的判断

      function isApiError(error: Error) {
          if (error instanceof ApiError) {
              return true;
          }
          return false;
      }
      
      // 报错: error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
      

8.2.3 将任何一个类型断言为any

一个小栗子:

  • 我们的需求:给window添加一个属性foo,我们写的代码如下:
    window.foo = 1;
    
    // 报错啦: - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
    
    • 报错提示window上不存在属性foo
  • 解决方法:使用as any临时将window断言为any类型:
    (window as any).foo = 1;
    

any类型的变量上,访问任何属性都是允许的。
需要注意的是:将一个变量断言为any可以说是解决TS类型问题的最后一个手段。
总之,一方面不能滥用as any,另一方面也不要完全否认它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡。

8.2.4 将any断言为一个具体的类型

遇到any类型的变量时,我们可以选择无视他,任由它滋生出更多的any,也可以选择改进它,通过类型断言及时的把any断言为精确的类型。

举个栗子:

  • 历史遗留的代码中有个getCacheData,它的返回值是any
    function getCacheData(key: string): any {
        return (window as any).cache[key];
    }
    
  • 那我们在使用它时,最好能够在调用它之后,将它的返回值断言成一个精确的类型,这样就方便了后续的操作:
    function getCacheData(key: string): any {
        return (window as any).cache[key];
    }
    
    interface Cat {
        name: string;
        run(): void;
    }
    
    // 我们调用完 getCacheData 之后,立即将它断言为 Cat 类型
    const tom = getCacheData('tom') as Cat;
    tom.run();
    

8.3 类型断言的限制

8.4 双重断言

写在最前面——若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。除非迫不得已,千万别用双重断言


既然,任何类型都可以被断言为any,any可以被断言为任何类型,那我们是不是可以用双重断言来将任何一个类型断言为任何另一个类型

举个栗子:

interface Cat {
    run(): void;
}
interface Fish {
    swim(): void;
}

function testCat(cat: Cat) {
    return (cat as any as Fish);    //如果直接使用 cat as Fish 肯定会报错,因为 Cat 和 Fish 互相都不兼容

8.5 类型断言 VS 类型转换

  • 类型断言只会影响TS编译时的类型,类型断言语句在编译结果中会被删除

  • 类型断言不是类型转换,它不会真的影响到变量的类型

  • 几个栗子:

    • 下面的代码,将something断言为boolean,虽然可以通过编译,但是并没有什么用
    function toBoolean(something: any): boolean {
        return something as boolean;
    }
    
    toBoolean(1);
    // 返回值为 1
    

    编译后的:

    function toBoolean(something) {
        return something;
    }
    
    toBoolean(1);    //类型断言不会真的影响变量的类型
    // 返回值为 1
    
    • 类型转换
    function toBoolean(something: any): boolean {
        return Boolean(something);
    }
    
    toBoolean(1);    //浅浅做个对比
    // 返回值为 true
    

8.6 类型断言 VS 类型声明

举个栗子说说类型断言和类型声明的区别:

  • 同——两者都能达到的效果:
    • 使用 as Catany 类型断言为了 Cat 类型
      function getCacheData(key: string): any {
          return (window as any).cache[key];
      }
      
      interface Cat {
          name: string;
          run(): void;
      }
      
      const tom = getCacheData('tom') as Cat;
      tom.run();
      
    • 使用类型声明也能达到上面的效果
 //将 tom 声明为 Cat,然后再将 any 类型的 getCacheData('tom') 赋值给 Cat 类型的 tom
const tom: Cat = getCacheData('tom');   
tom.run();
  • 异——两者的区别
    • 使用类型断言
    interface Animal {
        name: string;
    }
    interface Cat {
        name: string;
        run(): void;
    }
    
    const animal: Animal = {
        name: 'tom'
    };
    
    // 由于 Animal 兼容 Cat,故可以将 animal 断言为 Cat 赋值给 tom
    let tom = animal as Cat;
    
    • 使用类型声明(会报错)
    
    //报错原因:Animal 可以看作是 Cat 的父类,当然不能将父类的实例(animal)赋值给类型为子类(Cat)的变量
    let tom: Cat = animal;
    
    // - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
    
  • 这个栗子中,类型断言和类型声明的核心区别:
    • animal断言为Cat,只需要满足 Animal兼容CatCat兼容 Animal即可
    • animal赋值给tom,需要满足Cat兼容 Animal才行
    • 在前一个栗子中,由于getCacheData('tom')any类型,any兼容CatCat也兼容any,所以
    const tom = getCacheData('tom') as Cat;
    //等价于
    //const tom: Cat = getCacheData('tom');
    

通过上面的例子,我们知道了类型声明比类型断言更加严格,所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的as语法更优雅。

8.7 类型断言 VS 泛型

9. 声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

9.1 新语法

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// 三斜线指令

9.2 什么是声明语句

假如我们想要使用第三方库jQuery,一种常见的方式是在html中通过

你可能感兴趣的:(TypeScript,typescript,学习,javascript)