TypeScript 系列三

  • 三、TypeScript 断言
  • 3.1 类型断言
  • 1. "尖括号"语法
  • 2. as 语法
  • 3.2 非空断言
  • 1. 忽略 undefined 和 null 类型
  • 2. 调用函数时忽略 undefined 类型
  • 3.3 确定赋值断言
  • 四、类型守卫
  • 4.1 in 关键字
  • 4.2 typeof 关键字
  • 4.3 instanceof 关键字
  • 4.4 自动以类型保护的类型谓词
  • 五、联合类型和类型别名
  • 5.1 联合类型
  • 5.2 可辨识联合
  • 1. 可辨识
  • 2. 联合类型
  • 3. 类型守卫
  • 5.3 类型别名
  • 六、交叉类型
  • 6.1 同名基础类型属性的合并
  • 6.2 同名非基础类型属性的合并
  • 未完待续!!!


三、TypeScript 断言

3.1 类型断言

有时候你会遇到这样的情况,你会⽐ TypeScript 更了解某个值的详细信息。通常这会发⽣在你清楚地知道⼀个实体具有⽐它现有类型更确切的类型。

通过类型断⾔这种⽅式可以告诉编译器,“相信我,我知道⾃⼰在⼲什么”。类型断⾔好⽐其他语⾔⾥的类型转换,但是不进⾏特殊的数据检查和解构。它没有运⾏时的影响,只是在编译阶段起作⽤。

1. "尖括号"语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length
2. as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length

3.2 非空断言

在上下⽂中当类型检查器⽆法断定类型时,⼀个新的后缀表达式操作符 ! 可以⽤于断⾔操作对象是⾮null 和⾮ undefined 类型。具体而言,x! 将从x值域中排除null和undefined。

那么⾮空断⾔操作符到底有什么⽤呢?下⾯我们先来看⼀下⾮空断⾔操作符的⼀些使⽤场景。

1. 忽略 undefined 和 null 类型
function myFunc(maybeString: string | undefined null) {
	// Type 'string | null | undefined' is not assignable to type 'string'.
 	// Type 'undefined' is not assignable to type 'string'.
 	const onlyString: string = maybeString; // Error
 	const ignoreUndefinedAndNull: string = maybeString!; // OK
}
2. 调用函数时忽略 undefined 类型
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
	// Object is possibly 'undefined'.(2532)
 	// Cannot invoke an object which is possibly 'undefined'.(2722)
 	const num1 = numGenerator(); // Error
 	const num2 = numGenerator!(); // OK
}

因为 ! ⾮空断⾔操作符会从编译⽣成的 JavaScript 代码中移除,所以在实际使⽤的过程中,要特别注意。⽐如下⾯这个例⼦:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b);

以上 TS 代码会编译⽣成以下 ES5 代码:

"use strict"
const a = undefined;
const b = a;
console.log(b);

虽然在 TS 代码中,我们使⽤了⾮空断⾔,使得 const b: number = a!; 语句可以通过 TypeScript 类型检查器的检查。但在⽣成的 ES5 代码中, ! ⾮空断⾔操作符被移除了,所以在浏览器中执⾏以上代
码,在控制台会输出 undefined 。

3.3 确定赋值断言

在 TypeScript 2.7 版本中引⼊了确定赋值断⾔,即允许在实例属性和变量声明后⾯放置⼀个 ! 号,从⽽告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作⽤,我们来看个具体的例⼦:

let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
	x = 10;
}

很明显该异常信息是说变量 x 在赋值前被使⽤了,要解决该问题,我们可以使⽤确定赋值断⾔:

let x!: number;
initialize();
console.log(2 * x); // OK
function initialize() {
	x = 10;
}

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确赋值。

四、类型守卫

类型保护是可执行运行时检查的⼀种表达式,用于确保该类型在⼀定的范围内。 换句话说,类型保护可以保证⼀个字符串是⼀个字符串,尽管它的值也可以是⼀个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、⽅法或原型,以确定如何处理值。

⽬前主要有四种的⽅式来实现类型保护:

4.1 in 关键字

interface Admin {
	name: string;
	privileges: string[];
}

interface Employee {
	name: string;
	startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
	console.log("Name" + emp.name);
	if("privileges" in emp) {
		console.log("Privileges: " + emp.privileges);
	}
	
	if("startDate" in emp) {
		console.log("Start Date: " + emp.startDate);
	}
}

4.2 typeof 关键字

function padLeft(value: string, padding: string | number) {
 if (typeof padding === "number") {
 return Array(padding + 1).join(" ") + value;
 }
 if (typeof padding === "string") {
 return padding + value;
 }
 throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型保护只支持两种形式:typeof v === “typename”typeof v !== typename“typename” 必须是 “number”“string”“boolean”“symbol”。但是TypeScript 并不会阻止你与其他字符串比较,语言不会吧哪些表达式识别为类型保护。

4.3 instanceof 关键字

interface Padder {
	getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
 constructor(private numSpaces: number) {}
 getPaddingString() {
 return Array(this.numSpaces + 1).join(" ");
 }
}

class StringPadder implements Padder {
 constructor(private value: string) {}
 getPaddingString() {
 return this.value;
 }
}

let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
 // padder的类型收窄为 'SpaceRepeatingPadder'
}

4.4 自动以类型保护的类型谓词

function isNumber(x: any): x is number {
	return typeof x === "number";
}

function isString(x: any): x is string {
	return typeof x === "string"
}

五、联合类型和类型别名

5.1 联合类型

联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => {
	/*...*/
}

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给 sayHello 函数。

sayHello("semlinker");
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的⽤法:

let num: 1 | 2 = 1;
type EventName = 'click' | 'scroll' | 'mousemove';

以上实例中的 12‘click’ 被称为字面量类型,用来约束取值只能是某几个值中的一个。

5.2 可辨识联合

TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含3 个要点:可辨识、联合类型和类型守卫。

这种类型的本质是结合联合类型和字⾯量类型的⼀种类型保护⽅法。如果⼀个类型是多个类型的联合类型,且多个类型含有⼀个公共属性,那么就可以利⽤这个公共属性,来创建不同的类型保护区块。

1. 可辨识

可辨识要求联合类型中的每个元素都含有⼀个单例类型属性,⽐如:

enum CarTransmission {
	Automatic = 200,
	Manual = 300
}

interface Motorcycle {
	vType: "motorcycle"; // discriminant
	make: number; // year
}

interface car {
	vType: "car"; // discriminant
	transmission: CarTransmission
}

 interface Truck {
	vType: "truck"; // discriminant
	capacity: number; // in tons	
 }

在上述代码中,我们分别定义了Motorcycle,Car和Truck三个接口,这些接口中都包含一个vType属性,该属性被称为可辨识的属性,而其他的属性只跟特性的接口相关。

2. 联合类型

基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:

type Vehicle = Motorcycle | Car | Truck;

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。

3. 类型守卫

下⾯我们来定义⼀个 evaluatePrice ⽅法,该⽅法⽤于根据⻋辆的类型、容量和评估因⼦来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI;

function evaluatePrice(vehicle: Vehicle) {
	return vehicle.capacity * EVALUATION_FACTOR
}

const myTruck: Truck = {
	vType: "truck",
	capacity:9.5
}

evaluatePrice(myTruck)

对于以上代码,TypeScript 编译器将会提示以下错误信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接⼝中,并不存在 capacity 属性,⽽对于 Car 接⼝来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使⽤类型守卫。下⾯我们来重构⼀下前⾯定义的 evaluatePrice ⽅法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
	switch(vehicle.vType) {
		case "car":
			return vehicle.transmission * EVALUATION_FACTOR;
		case "truck":
			return vehicle.capacity * EVALUATION_FACTOR;
		case "motorcycle":
			return vehicle.make * EVALUTION_FACTOR;
	}
}

在以上代码中,我们使⽤ switchcase 运算符来实现类型守卫,从⽽确保在 evaluatePrice ⽅法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该⻋辆类型所对应的价格。

5.3 类型别名

类型别名用来给一个类型起个新名字。

type Message = string | string[];

let great = (message: Message) => {
	// ...
}

六、交叉类型

在 TypeScript 中交叉类型是将多个类型合并为⼀个类型。通过 & 运算符可以将现有的多种类型叠加到⼀起成为⼀种类型,它包含了所需的所有类型的特性。

type PartialPointX = {
	x: number;
}

type Point = PartialPointX & { y: number };

let point: Point = {
	x: 1,
	y: 1
}

在上⾯代码中我们先定义了 PartialPointX 类型,接着使⽤ & 运算符创建⼀个新的 Point 类型,表示⼀个含有 xy 坐标的点,然后定义了⼀个 Point 类型的变量并初始化。

6.1 同名基础类型属性的合并

那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型⼜不⼀致,⽐如:

interface X {
	c: string;
	d: string;
}

interface Y {
	c: number;
	e: string;
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;

在上⾯的代码中,接⼝ X 和接⼝ Y 都含有⼀个相同的成员 c,但它们的类型不⼀致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 stringnumber 类型呢?⽐如下⾯的例⼦:

P = {
	c: 6,
	d: "d",
	e: "e"
}

上述代码执行完,会出现以下错误:

Type 'number' is not assignable to type 'never'.
Type 'string' is not assignable to type 'never'.

为什么接⼝ X 和接⼝ Y 混⼊后,成员 c 的类型会变成 never 呢?这是因为混⼊后成员 c 的类型为 string & number ,即成员 c 的类型既可以是 string 类型⼜可以是 number 类型。很明显这种类型是不存在的,所以混⼊后成员 c 的类型为 never

6.2 同名非基础类型属性的合并

在上⾯示例中,刚好接⼝ X 和接⼝ Y 中内部成员 c 的类型都是基本数据类型,那么如果是⾮基本数据类型的话,⼜会是什么情形。我们来看个具体的例⼦:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
 x: {
 d: true,
 e: 'semlinker',
 f: 666
 }
};

console.log('abc:', abc);

在控制台中输出的结果可知,在混⼊多个类型时,若存在相同的成员,且成员类型为⾮基本数据类型,那么是可以成功合并。