TypeScript实战
上QQ阅读APP看书,第一时间看更新

2.3 类型

TypeScript可以说是一门具有面向对象特征的静态类型语言,可以用来描述现实世界中的对象。不同的对象具有不同的属性和行为。一般来说,现实中的某个事物具有不同类型的属性,以人这个对象为例,有身高属性、名字属性、性别属性和是否结婚等属性,这些属性的值分别是数值型、字符型、枚举型和布尔型。

提示

TypeScript语言的静态类型系统(Type System)在程序编译阶段就可以检查类型的有效性,这可以确保代码按预期运行。

编程语言若没有类型,则无法确切描述现实对象,也就失去了编程语言的价值。因此,类型对于任何一门语言而言都是核心基础。TypeScript中的所有类型都是any类型的子类型。any类型可以表示任何值。根据官方的《TypeScript Language Specification》文档描述,TypeScript数据类型除了any类型外,其他的可以分类为原始类型(primitive types)、对象类型(object types)、联合类型(union types)、交叉类型(intersection types)和类型参数(type parameters)。另外,数据类型又可以分为内置类型(如数值类型)和用户自定义类型(如枚举类型和类等)。

本节介绍TypeScript语言的一些常用类型。

2.3.1 基础类型

TypeScript的原始类型(primitive types)有数值型(number)、布尔型(boolean)、字符型(string)、符号型(symbol)、void型、null型、undefined型和用户自定义的枚举类型8种。本小节重点对数值型、布尔型和字符型这些基础类型进行阐述。

1.数值型

TypeScript中的数值型和JavaScript一样,是双精度64位浮点值。它可以用来表示整数和分数,在TypeScript中并没有整数型。注意,在有些金融计算领域,如果对于精度要求较高,就需要注意计算误差的问题。

一般来说,判定一个变量是否需要设置成数值类型,可以看它是否需要进行四则运算,如果一个变量可以加减乘除,那么这个变量很可能就是数值类型,如金额。

数值类型的变量可以存储不同进制的数值,默认是十进制,也可以用0b表示二进制、0o表示八进制、0x表示十六进制。

下面给出几种TypeScript中数值型变量常见的声明语法,示例如代码2-2所示。

【代码2-2】数值类型声明示例:numbers.ts

    01  let num1: number = 89.2 ;        //分数,十进制
    02  let int2 : number = 2 ;          //整数,十进制
    03  let binaryVar: number = 0b1010;  //二进制
    04  let octalVar: number = 0o744;    //八进制
    05  let hexVar: number = 0xf00d;     //十六进制

提示

变量声明数值类型后,不允许将字符类型或布尔类型等不兼容类型赋值给此变量。

01行用“let num1: number”声明了一个名为num1的数值(number )类型变量。然后用=对变量num1进行初始化,初始值为89.2,这个值并没有加前缀,因此默认是十进制的。这是最常用的一种数值进制。

另外需要注意的是,TypeScript中有Number和number两种类型,但是它们是不一样的,Number是number类型的封装对象。Number对象的值是number类型。

2.布尔型

布尔型数据表示逻辑值,值只能是true或false。布尔值是最基础的数据类型,在TypeScript中,使用boolean关键词来定义布尔类型,示例如代码2-3所示。

【代码2-3】布尔类型声明示例:boolean.ts

    01  let isMan: boolean = true ;
    02  let isBoy : boolean = false ;

01行用“let isMan:boolean”声明了一个名为isMan的布尔(boolean)类型变量。然后用=对变量isMan进行初始化,初始值为true,这个值只能是true或false,不能是1或者0等。

另外需要注意的是,TypeScript中有Boolean和boolean两种类型,但是它们是不一样的,Boolean是boolean类型的封装对象,Boolean对象的值是boolean类型。从下面给出的示例代码2-4可以看出,二者是存在差异的。

【代码2-4】布尔类型示例:boolean2.ts

    01  let a : boolean = new Boolean(1) ;  //错误
    02  let b : boolean = Boolean(1) ;      //正确

从代码2-4可以看出,在TypeScript中,boolean类型和new Boolean(1)是不兼容的。但是和直接调用Boolean(1)是兼容的。

提示

在TypeScript中,Number、String和Boolean分别是number、string和boolean的封装对象。

3.字符型

字符型(字符串类型)表示Unicode字符序列,可以用单引号'或者双引号"来表示字符。不过一般建议用双引号来表示字符。二者可以互相嵌套使用。\符号可以对"等进行转义。从下面给出的示例代码2-5可以看出,单引号'或者双引号"都可以给字符类型的变量进行赋值。

【代码2-5】字符类型示例:string.ts

    01  let msg: string= " hello world ";
    02  let msg2: string= ' hello world ';
    03  let a = "I'M ok" ;
    04  let b = 'hello "world" ' ;
    05  let c = "\"helo\"" ;

模板字符串(template string)是增强版的字符串,用反引号`标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

下面的代码2-6给出了模板字符串示例。使用模板字符串的一个最大好处就是可以防止传统的变量和字符通过+进行拼接的时候单引号和双引号相互嵌套所导致的不容易发现拼接错误的问题。

【代码2-6】模板字符串示例:string_template.ts

    01  let name: string = "JackYunDi";
    02  let age: number = 2;
    03  let msg: string = '今年 ${name}已经${age}岁了';//今年JackYunDi已经2岁了
    04  let b = '
    05      hello typescript
    06      hello world
    07  '; //多行文本

提示

字符串可以通过索引来获取值中对应的字符,如"hello"[0]输出h。

另外,字符串可以通过+进行字符拼接,这个操作在日常的编程实战中也是非常常见的用法。代码2-7给出了字符串拼接示例。

【代码2-7】字符串+拼接示例:string_join.ts

    01  let firstName: string = "Jack";
    02  let lastName: string = "Wang";
    03  let fullName = firstName + " " + lastName;
    04  console.log(fullName);        //Jack Wang

如果字符串和数字用+字符连接,那么结果将成为字符串。因此可以将空字符串和数值相加用于将数值类型转化成字符类型,如代码2-8所示。

【代码2-8】字符串和数字拼接示例:string_join2.ts

    01  let res = "" + 5;
    02  console.log(res );    //"5"

另外,字符串和布尔型用+进行拼接,也生成字符串,如true +""的值为"true" 。

如果字符串和数组用+字符连接,那么结果将成为字符串。因此可以将空字符串和数组相加用于将数值的值转成用(,)分隔的一个字符串,如代码2-9所示。

【代码2-9】字符串和数组拼接示例:string_join3.ts

    01  let res = "" + [1,2,3];
    02  console.log(res );//"1,2,3"

2.3.2 枚举

TypeScript语言支持枚举(enum)类型。枚举类型是对JavaScript标准数据类型的一个补充。枚举用于取值被限定在一定范围内的场景,比如一周只能有7天,彩虹的颜色限定为赤、橙、黄、绿、青、蓝、紫,这些都适合用枚举来表示。

TypeScript可以像C#语言一样,可以使用枚举类型为一组数值赋予更加友好的名称,从而提升代码的可读性,枚举使用enum关键字来定义,代码2-10给出了枚举的示例。

【代码2-10】枚举示例:enums.ts

    01  enum Days {Sun, Mon, Tue, Wed, Thu, Fr i, Sat};
    02  let today: Days = Days.Sun;

在代码2-10中,01行用enum关键词声明了一个名为Days的枚举类型,一般枚举类型的标识符首字母大写。02行用自定义的枚举类型来声明一个新的变量today,并赋值为Days.Sun 。使用枚举可以限定我们的赋值范围,防止赋值错误,例如不能为today变量赋值为Days.OneDay。

提示

一般情况下,枚举类型的变量本质上只是数值。Days.Sun实际上是0。“let today: Days =2 ;”也没有语法错误。

默认情况下,枚举中的元素从0开始编号。同时也会对枚举值到枚举名进行反向映射。代码2-11给出了枚举值和枚举名之间的映射关系用法。

【代码2-11】枚举值和枚举名映射示例:enums2.ts

    01  enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
    02  console.log(Days["Sun"] === 0);             // true
    03  console.log(Days["Mon"] === 1);             // true
    04  console.log(Days["Tue"] === 2);             // true
    05  console.log(Days["Wed"] === 3);             // true
    06  console.log(Days["Sat"] === 6);             // true
    07  console.log(Days[0] === "Sun");             // true
    08  console.log(Days[1] === "Mon");             // true
    09  console.log(Days[2] === "Tue");             // true
    10  console.log(Days[6] === "Sat");             // true

可以根据实际情况,手动指定成员的索引数值(一般为整数,但是也可以是小数或负数)。例如,可以将上面的例子改成从1开始编号,枚举支持连续编号和不连续编号,也支持部分编号和部分不编号,如代码2-12所示。

【代码2-12】枚举索引编号示例:enums3.ts

    01  enum Days {Sun = 1, Mon = 2, Tue = 4 , Wed = 3 , Thu =5, Fri, Sat};
    02  console.log(Days["Sun"] === 1);       // true
    03  console.log(Days["Mon"] === 2);       // true
    04  console.log(Days["Tue"] === 4);       // true
    05  console.log(Days["Wed"] === 3);       // true
    06  console.log(Days["Thu"] === 5);       // true
    07  console.log(Days["Fri"] === 6);       // true
    08  console.log(Days["Sat"] === 7);       // true

提示

给枚举类型进行手动赋值时,一定要注意手动编号和自动编号不要重复,否则会相互覆盖。

枚举手动编号和自动编号如果出现重复,那么重复的枚举名会指向同一个值,而这个数值只会返回最后一个赋值的枚举名。TypeScript编译器并不会提示错误或警告。这种情况如代码2-13所示。

【代码2-13】枚举索引编号示例:enums4.ts

    01  enum Days {Sun = 1, Mon = 2, Tue = 4 , Wed = 3 , Thu , Fri, Sat};
    02  console.log(Days["Sun"] === 1);      // true
    03  console.log(Days["Mon"] === 2);      // true
    04  console.log(Days["Tue"] === 4);      // true
    05  console.log(Days.Tue);               // 4
    06  console.log(Days.Thu);               // 4
    07  console.log(Days.Fri);               // 5
    08  console.log(Days.Sat);               // 6
    09  console.log(Days[4]);                // Thu

在代码2-13的例子中,Wed = 3后,并未手动进行编号,系统自动编号为递增编号,即Thu是4、Fri是5、Sat是6。但是TypeScript并没有报错,导致Days[4]的值先是Tue而后又被Thu覆盖了。因此Days[4]的值为Thu。

通常情况下,枚举名的赋值一般为数值,但是手动赋值的枚举名可以不是数字,如字符串。此时需要使用类型断言(这部分内容将在后续章节进行说明)来让tsc无视类型检查。代码2-14演示了枚举索引用字符串进行编号。

【代码2-14】枚举索引编号示例:enums5.ts

    01  enum Days {Sun = <any>"S", Mon = 2, Tue = 4 , Wed  , Thu , Fri, Sat};
    02  console.log(Days["Sun"] === <any>"S"); // true
    03  console.log(Days["Mon"] === 2);      // true
    04  console.log(Days["Tue"] === 4);      // true
    05  console.log(Days["S"]);              //Sun

另外,在声明枚举类型时,可以在关键词enum前加上const来限定此枚举是一个常数枚举。常数枚举的示例见代码2-15所示。

【代码2-15】常数枚举示例:enums6.ts

    01  const enum Directions {
    02      Up,
    03      Down,
    04      Left,
    05      Right
    06  }
    07  let directions: Directions = Directions.Up;
    08  console.log(directions);     // 0

常数枚举与普通枚举的区别是,它会在编译阶段被删除。代码2-15如果用tsc编译成JavaScript,内容如代码2-16所示。

【代码2-16】常数枚举编译JavaScript示例:enums6.js

    01  var directions = 0 /* Up */;
    02  console.log(directions);

代码2-15如果去掉const关键词,用tsc编译成JavaScript,内容如代码2-17所示。

【代码2-17】普通枚举编译JavaScript示例:enums6_2.js

    01  var Directions;
    02  (function (Directions) {
    03      Directions[Directions["Up"] = 0] = "Up";
    04      Directions[Directions["Down"] = 1] = "Down";
    05      Directions[Directions["Left"] = 2] = "Left";
    06      Directions[Directions["Right"] = 3] = "Right";
    07  })(Directions || (Directions = {}));
    08  var directions = Directions.Up;
    09  console.log(directions);

外部枚举(Ambient Enums)是使用declare enum定义的枚举类型,declare定义的类型只会用于编译时的检查,编译成JavaScript后会被删除。因此,外部枚举与声明语句一样,常出现在声明文件(关于声明文件将在后续章节进行详细说明)中。外部枚举示例如代码2-18所示。

【代码2-18】外部枚举示例:enums7.ts

    01  declare  enum Directions {
    02      Up,
    03      Down,
    04      Left,
    05      Right
    06  }
    07  let directions : Directions = Directions.Up;
    08  console.log(directions);

将代码2-18中的代码编译成JavaScript代码,内容如代码2-19所示。

【代码2-19】外部枚举编译成JavaScript示例:enums7.js

    01  var directions = Directions.Up; //Directions未定义
    02  console.log(directions);

提示

从代码2-19可以看出,外部枚举定义的枚举在生成JavaScript的时候会整段进行自动删除,从而出现Directions未定义的情况。

2.3.3 任意值

TypeScript语言是一种静态类型的JavaScript,可以更好地进行编译检查和代码分析等,但有些时候TypeScript需要和JavaScript库进行交互,这时就需要任意值(any)类型。

在某些情况下,编程阶段还不清楚要声明的变量是什么类型,这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。这种情况下,我们不希望类型检查器对这些值进行检查,此时可以声明一个任意值类型的变量。任意值类型示例如代码2-20所示。

【代码2-20】任意值示例:any.ts

    01  let myVar: any = 7;
    02  myVar= "maybe a string instead";
    03  myVar= false;

从代码2-20可以看出,任意值变量myVar初始化值为数值7,然后对其赋值字符串"maybe a string instead"和布尔值false,都可以编译通过。

由于任意值类型允许我们在编译时可选择地包含或移除类型检查,因此在对现有代码进行改写的时候,任意值类型是十分有用的。但是由于any类型不让编译器进行类型检查,一般尽量不使用,除非必须使用它才能解决问题。

提示

any类型上没有任何内置的属性和方法可以被调用,它只能在运行时检测该属性或方法是否存在。因此声明一个变量为任意值之后,编译器无法帮助你进行类型检测和代码提示。

任意值类型和Object看起来有相似的作用,但是Object类型的变量只是允许你给它赋不同类型的值,但是却不能够在它上面调用可能存在的方法,即便它真的有这些方法。代码2-21给出了对比二者的示例。

【代码2-21】 any和Object对比示例:any_Object.ts

    01  let notSure: any = 4;
    02  notSure.ifItExists();            // ifItExist方法在运行时可能存在
    03  notSure.toFixed();           // toFixed是数值4的方法
    04  let prettySure: Object = 4;      // 此处是大写的Object,不是小写的object
    05  prettySure.toFixed();        // 错误Object类型没有toFixed方法

从代码2-21可以看出,any类型的变量可以调用任何方法和属性,但是Object类型的变量却不允许调用此类型之外的任何属性和方法,即使Object对象有这个属性或方法也不允许。

提示

代码2-21中02行在编码阶段是没有错误的,但是当编译成JavaScript运行时,就会报ifItExists方法不存在的错误。

另外,当只知道一部分数据的类型时,any类型也是有用的。比如,你有一个列表,它包含了不同类型的数据,那么我可以用任意值数组来进行存储,如代码2-22所示。

【代码2-22】 any数组示例:any_array.ts

    01  let list: any[] = [1, true, "free"];
    02  list[1] = 100;

提示

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

TypeScript会在没有明确地指定类型的时候推测出一个类型,这就是类型推论。如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成any类型而完全不被编译器进行类型检查,如代码2-23所示。

【代码2-23】 any类型推论示例:any_infer.ts

    01  let some;      //any 类型
    02  some = 'Seven';
    03  some = 7;
    04  some.getName();

在代码2-23中,01行只是声明了一个变量some,但是并未明确指定其类型,也没有赋值,因此变量some会被推断为any类型。

2.3.4 空值、Null与Undefined

这里将空值(void)、Null与Undefined放在一起来介绍,主要是由于它们有容易混淆的地方。下面将依次对void、Null与Undefined类型进行详细说明。

1.空值

空值(void)表示不返回任何值,一般在函数返回类型上使用,以表示没有返回值的函数。JavaScript没有空值类型。在TypeScript中,可以用void关键词表示没有任何返回值的函数,如代码2-24所示。

【代码2-24】 void函数示例:void_func.ts

    01  function hello():void{
    02     console.log("void类型");
    03  }

void一般都可以省略,从而简化代码。声明一个void类型的变量没有什么实际用途,因为你只能将它赋值为undefined和null。而且一个void类型的变量也不能赋值到其他类型上(除了any类型以外),如代码2-25所示。

【代码2-25】 void变量示例:void_var.ts

    01  let vu: void = null;
    02  let vu2: void =undefined;
    03  let num: number = vu;      // 错误,不能将void赋值到number
    04  let num2: any= vu;         // 正确
2.null

null表示不存在对象值。在TypeScript中,可以使用null关键词来定义一个原始数据类型,但要注意这本身没有实际意义。null一般当作值来用,而不是当作类型来用。

    let n: null = null; //无意义

null类型的变量可以被赋值为null或undefined或any,其他值不能对其进行赋值。代码2-26给出了null类型的变量用法。

【代码2-26】 null变量示例:null_var.ts

    01  let uv: null = null;
    02  let uv2: null = undefined;
    03  let uv3: null = 2;         //错误
    04  let a:any = 2 ;
    05  uv = a ;
    06  console.log(a);        //2

从代码2-26可以看出,null既可以是数据类型也可以是值。null类型的变量不能将数值2赋值给它,但是可以赋值any类型的变量,而any类型的变量可以赋值为2。因此上述的06行输出2。

3.undefined

undefined表示变量已经声明但是尚未初始化变量的值。undefined和null是所有类型的子类型。也就是说undefined和null类型的变量可以赋值给所有类型的变量。和null一样,在TypeScript中可以使用undefined来定义一个原始数据类型,但要注意这没有实际意义,undefined一般当作值来用。

    let n: undefined = undefined ;  //无意义

undefined类型的变量可以被赋值为null或undefined或any,其他值不能对其进行赋值。代码2-27给出了undefined类型的变量用法。

【代码2-27】 undefined变量示例:undefined_var.ts

    01  let uv: undefined = undefined ;
    02  let uv2: undefined = null;
    03  let uv3: undefined = 3;        //错误
    04  let a: any = 6;
    05  uv = a ;
    06  console.log(uv);            //6;

从代码2-27可以看出,undefined既可以是数据类型也可以是值。undefined类型的变量不能将数值6赋给它,但是可以赋值any类型的变量,而any类型的变量可以赋值为6。因此代码2-27中的06行输出6。

综上可知,声明一个void类型,undefined类型和null类型的变量其实是没有什么意义的,因为你只能为它赋予undefined和null,当然也可以是any。

void一般只用于函数返回值上,但是经常省略。undefined和null一般都是当作值来使用的,undefined表示变量已经声明但是未初始化变量的值,而null表示值初始化为null。默认情况下,null和undefined是所有类型的子类型。换句话说,你可以把null和undefined赋值给任何类型的变量。

提示

在编译时,如果开启了--strictNullChecks配置,那么null和undefined只能赋值给void和它们本身。这能避免很多常见的问题。

2.3.5 Never

Never类型是其他类型(包括null和undefined)的子类型,代表从不会出现的值。Never类型只能赋值给自身,其他任何类型不能给其赋值,包括任意值类型。Never类型在TypeScript中的类型关键词是never。

Never类型一般出现在函数抛出异常Error或存在无法正常结束(死循环)的情况下。代码2-28给出返回Never类型的函数示例。

【代码2-28】 Never类型的函数示例:never.ts

    01  // 返回never的函数
    02  function error(message: string): never {
    03      throw new Error(message);
    04  }
    05  // 推断返回值类型为never
    06  function fail() {
    07      return error("Something failed");
    08  }
    09  // 返回never的函数必须存在无法结束
    10  function infiniteLoop(): never {
    11      while (true) {
    12      }
    13  }

2.3.6 Symbols

自ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种原始数据类型。

Symbol类型的变量一旦创建就不可变更,且不能为它设置属性。Symbol一般是用作对象的一个属性。即使两个Symbol声明的时候是同名的,也不是同一个变量,这样就能避免命名冲突的问题。

Symbol类型的值是通过Symbol构造函数进行创建的。代码2-29给出了Symbol类型的变量声明示例。

【代码2-29】 Symbol类型的示例:symbol.ts

    01  let s1 = Symbol('name');    // Symbol()
    02  let s2 = Symbol('age');
    03  console.log(s1)             // Symbol(name)
    04  console.log(s2)             // Symbol(age)
    05  console.log(s1.toString())  // "Symbol(name)"
    06  console.log(s2.toString())  // "Symbol(age)"

Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述。这个描述主要是为了在控制台显示,或者转为字符串时比较容易区分不同的Symbol变量。

提示

Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,因此不能添加属性。

Symbol是不可改变且唯一的,Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的,如代码2-30所示。

【代码2-30】 Symbol类型的示例:symbol2.ts

    01  // 没有参数的情况
    02  let s1 = Symbol();
    03  let s2 = Symbol();
    04  console.log(s1 === s2)      // false
    05  // 有参数的情况
    06  let s3 = Symbol('age');
    07  let s4 = Symbol('age');
    08  console.log(s3 === s4)      // false

像字符串一样,Symbol也可以被用作对象属性的键等。对象的属性名现在可以有两种类型,一种是字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型的就表示这个属性是独一无二的。这个特征可以保证不会与其他同名属性产生冲突。代码2-31给出了Symbol类型作为属性的示例。

【代码2-31】 Symbol类型作为属性的示例:symbol3.ts

    01  let sym = Symbol("name");
    02  let sym2 = Symbol("name");
    03  let obj = {
    04      [sym]: "value",
    05      [sym2]: "value2",
    06      name:"value3"
    07  }; // 作为对象属性的键
    08  console.log(obj);

代码2-31编译成JavaScript后在浏览器中运行可以打印出如下信息:

提示

调用obj[sym]时报错,提示Type 'symbol' cannot be used as an index type,但是生成的JavaScript可以正确输出。

Symbol值不能与其他类型的值进行运算,会报错,如代码2-32所示。

【代码2-32】 Symbol与其他类型运算示例:symbol4.ts

    01  let s3 = Symbol('age');
    02  let s4 = s3+"是symbol";    //错误
    03  console.log(s4)

Symbol值可以显式转为字符串和布尔值,但是不能转为数值,如代码2-33所示。

【代码2-33】 Symbol显示转换示例:symbol5.ts

    01  let s3 = Symbol('age');
    02  let s4 = String(s3);       //Symbol(age)
    03  let s5 = Boolean(s3);      //true
    04  console.log(s4)
    05  console.log(s5)
    06  let s6 = Number(s3);       //错误
    07  console.log(s6)

2.3.7 交叉类型

交叉类型(Intersection Types)可以将多个类型合并为一个类型。合并后的交叉类型包含了其中所有类型的特性。以经典的动画片葫芦娃来举例,每个葫芦娃都有自己的特长,7个葫芦娃可合体为葫芦小金刚,他拥有所有葫芦娃的技能,非常强大。

下面假设有两个自定义类型,一个是Car类型,具备driverOnRoad功能;一个是Ship类型,具备driverInWater功能。我们通过交叉类型Car & Ship来合并功能,得到一个carShip对象。Car & Ship是Car类型和Ship类型的交叉类型,如代码2-34所示。

【代码2-34】交叉类型示例:intersection_types.ts

    01  class Car {
    02      public driverOnRoad() {
    03          console.log("can driver on road");
    04      }
    05  }
    06  class Ship {
    07      public driverInWater() {
    08          console.log("can driver in water");
    09      }
    10  }
    11  let car = new Car();
    12  let ship = new Ship();
    13  let carShip: Car & Ship = <Car & Ship>{};
    14  carShip["driverOnRoad"] = car["driverOnRoad"];
    15  carShip["driverInWater"] = ship["driverInWater"];
    16  carShip.driverInWater();//can driver in water
    17  carShip.driverOnRoad();//can driver on road

代码2-34例子中涉及类的相关知识,将在后续章节进行详细说明。这里读者不必细究。13行创建了一个Car & Ship类型的变量carShip,用{}进行了初始化,并用<Car & Ship>对空对象进行类型断言(将在2.3.9小节中进行详细说明),否则报错。

2.3.8 Union类型

联合类型(Union Types)表示取值可以为多种类型中的一种。联合类型与交叉类型在用法上完全不同。

假设有一个padLeft函数,让它可以在某个字符串的左边进行填充。该函数有两个参数:一个是需要被填充的字符串,是字符类型;另一个是要填充的对象,可以是number类型或string类型。

如果传入number类型的填充对象,那么在字符串左边填充number个空格;如果传入string类型的填充对象,那么在字符串左边填充该字符串即可。

为了实现第二个参数padding既可以是number类型也可以是string类型的效果,需要使用联合类型。number | string表示number和string的联合类型。下面的代码2-35给出了padLeft函数使用联合类型的示例。

【代码2-35】联合类型示例:union_types.ts

    01  function padLeft(value: string, padding: number | string) {
    02      if (typeof padding === "number") {
    03          return Array(padding + 1).join(" ") + value;
    04      }
    05      if (typeof padding === "string") {
    06          return padding + value;
    07      }
    08      throw new Error(`参数为string或number,但传入'${padding}'.`);
    09  }
    10  console.log(padLeft("Hello world", 3));             //    Hello world
    11  console.log(padLeft("Hello world", "__ "));         // __ Hello world
    12  //Argument of type 'true' is not assignable
    13  //to parameter of type 'string | number'.
    14  console.log(padLeft("Hello world", true));          // error

提示

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

还以上面的Car和Ship类型为例,我们扩展一个同名的方法toUpper,联合类型Car | Ship的实例就只能调用它们共有的方法toUpper,而driverOnRoad和driverInWater不能调用,具体示例如代码2-36所示。

【代码2-36】联合类型调用方法示例:union_types2.ts

    01  class Car {
    02      public driverOnRoad() {
    03          console.log("can driver on road");
    04      }
    05      public toUpper(str: string) {
    06          return str.toUpperCase();
    07      }
    08  }
    09  class Ship {
    10      public driverInWater() {
    11          console.log("can driver in water");
    12      }
    13      public toUpper(str2: string) {
    14          return str2.toUpperCase();
    15      }
    16  }
    17  let car = new Car();
    18  let ship = new Ship();
    19  let carShip: Car | Ship = <Car | Ship>{};
    20  carShip["driverOnRoad"] = car["driverOnRoad"];
    21  carShip["driverInWater"] = ship["driverInWater"];
    22  carShip["toUpper"] = ship["toUpper"];
    23  let str: string = carShip.toUpper("hello world");
    24  console.log(str);              //共有方法
    25  //carShip.driverOnRoad();      //不存在
    26  //carShip.driverInWater();     //不存在
    27  (<Car>carShip).driverOnRoad();     //OK
    28  (<Ship>carShip).driverInWater();   //OK

提示

可以用类型断言将Car | Ship断言成一个Car或者Ship类型的对象,从而调用特有的方法。

联合类型往往比较长,也不容易记忆和书写,我们可以用类型别名(Type Aliases)来解决这个问题。类型别名可以用来给一个类型起新名字,特别对于联合类型而言,起一个有意义的名字会让人更加容易理解。

类型别名的语法是:

    type 类型别名 = 类型或表达式;

类型别名可以用于简单类型和自定义类型,也可以用于表达式。我们可以用type给表达式()=> string起一个别名myfunc;也可以用type给联合类型string | number | myfunc起一个别名NameOrStringOrMyFunc。具体示例如代码2-37所示。

【代码2-37】类型别名示例:type_aliases.ts

    01  type myfunc = () => string;
    02  type NameOrStringOrMyFunc = string | number | myfunc;
    03  function getName(n: NameOrStringOrMyFunc): string {
    04      if (typeof n === 'string') {
    05          return n;
    06      }
    07      else if(typeof n === 'number'){
    08          return n.toString();
    09      }
    10      else {
    11          return n();
    12      }
    13  }
    14  let a :string = "hello";
    15  let b: number = 999;
    16  let c = function () {
    17      return "hello my func";
    18  }
    19  console.log(getName(a));        //hello
    20  console.log(getName(b));        //999
    21  console.log(getName(c));        //hello my func

另外,还可以给内置类型起一个别名,如string,如代码2-38所示。

【代码2-38】内置类型的类型别名示例:type_aliases2.ts

    01  let newString = string ;
    02  let a : newString = "new string type" ;

提示

虽然可以用type给内置类型起别名,但为了防止混淆,不建议给字符、数值或布尔等类型起别名。

最后,符号|也可以用于定义字符串字面量类型。这种类型用来约束字符的取值只能是某几个字符串中的一个,如用type定义一个表示事件的字符串字面量类型EventNames,并作为函数handleEvent的参数,这样此参数只能是'click'或'dbclick'或'mousemove',如代码2-39所示。

【代码2-39】字符串字面量类型示例:string_literal_type.ts

    01  type EventNames = 'click' | 'dbclick' | 'mousemove';
    02  function handleEvent(ele: Element, event: EventNames) {
    03      console.log(event);
    04  }
    05  let ele = document.getElementById('div'); //内置对象
    06  handleEvent(ele, 'click');                // 没问题
    07  handleEvent(ele, 'dbclick');              // 没问题
    08  handleEvent(ele, 'mousemove');            // 没问题
    09  handleEvent(ele, 'scroll');               // 不存在

提示

类型别名与字符串字面量类型都是使用type进行定义的,注意二者的区别。

2.3.9 类型断言

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

  •  <类型>值或者对象
  •  值或者对象as类型

提示

在tsx语法(React jsx语法的ts版)中必须用后一种,因此< >有特殊意义。

类型断言一般和联合类型一起使用,可以将一个联合类型的变量指定为一个更加具体的类型进行操作,从而可以使用特定类型的属性和方法。代码2-40给出了类型断言的示例。

【代码2-40】类型断言示例:type_assert.ts

    01  function getLength(a: string | number): number {
    02      //if ((a as string).length) {
    03      if ((<string>a).length) {
    04          return (<string>a).length;
    05      } else {
    06          return a.toString().length;
    07      }
    08  }
    09  console.log(getLength(6));             //1
    10  console.log(getLength("hello"));       //5

联合类型string | number限定参数a的类型,可以用类型断言<string>a指定类型为string,从而可以调用字符的length属性,如果传入的是数值,那么会返回a.toString().length的值。类型断言成一个联合类型string | number中不存在的类型(如boolean)是不允许的。

提示

类型断言不是类型转换,且类型推断不能直接进行调用,需要放于条件判断中或者先将其转化成unknown再进行类型断言,如console.log(<string><unknown>a).length)当a为数值时返回undefined。