本文共 18293 字,大约阅读时间需要 60 分钟。
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); }}
起别名不会新建一个类型 ,它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container= { value: T };
我们也可以使用类型别名来在属性里引用自己:
type Tree= { value: T; left: Tree ; right: Tree ;}
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
type LinkedList= T & { next: LinkedList };interface Person { name: string;}var people: LinkedList ;var s = people.name;var s = people.next.name;var s = people.next.next.name;var s = people.next.next.next.name;
我们使用 type 创建类型别名。类型别名常用于联合类型
类型别名不能出现在声明右侧的任何地方。接口 vs. 类型别名
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。type Alias = { num: number }interface Interface { num: number;}declare function aliased(arg: Alias): Alias;declare function interfaced(arg: Interface): Interface;
另一个重要区别是类型别名不能被 extends
和 implements
(自己也不能 extends
和 implements
其它类型)。 因为 ,你应该尽量去使用接口代替类型别名。
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // error! should not pass null or undefined. } }}let button = new UIElement();button.animate(0, 0, "ease-in");button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
元组起源于函数编程语言(如 F#),在这些语言中频繁使用元组。 定义一对值分别为string
和 number
的元组: let xcatliu: [string, number] = ['Xcat Liu', 25];
当赋值或访问一个已知索引的元素时,会得到正确的类型:
let xcatliu: [string, number];xcatliu[0] = 'Xcat Liu';xcatliu[1] = 25;xcatliu[0].slice(1);xcatliu[1].toFixed(2);
也可以只赋值其中一项:
let xcatliu: [string, number];xcatliu[0] = 'Xcat Liu';
但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
let xcatliu: [string, number];xcatliu = ['Xcat Liu', 25];//下面两种都会报错let xcatliu: [string, number] = ['Xcat Liu'];//这样也会报错let xcatliu: [string, number];xcatliu = ['Xcat Liu'];xcatliu[1] = 25;
越界的元素
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:let xcatliu: [string, number];xcatliu = ['Xcat Liu', 25];//这个不会报错xcatliu.push('http://xcatliu.com/');//这个会报错xcatliu.push(true);
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。枚举类型中是包含双向映射的,即(value -> name)和(name -> value)
数字枚举enum Direction { Up = 1, Down, Left, Right}console.info(Direction);//双向映射console.info(Direction.Down);//获取枚举的值console.info(Direction[2]); //获取枚举值对应的名称定义
如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说, Direction.Up的值为 1, Down为 2, Left为 3, Right为 4。
我们还可以完全不使用初始化器:enum Direction { Up, Down, Left, Right,}
使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:
enum Response { No = 0, Yes = 1,}function respond(recipient: string, message: Response): void { // ...}respond("Princess Caroline", Response.Yes)
字符串枚举
在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT",}
常数项和计算所得项
枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。//不会报错enum Color {Red, Green, Blue = "blue".length};//会报错enum Color {Red = "red".length, Green, Blue};
上面的例子中,"blue".length 就是一个计算所得项。
如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错。当满足以下条件时,枚举成员被当作是常数:
+
, -
,~
一元运算符应用于常数枚举表达式+
, -
, *
, /
, %
, <<
, >>
, >>>
, &
, |
, ^
二元运算符,常数枚举表达式作为其一个操作对象。若 常数枚举表达式求值后为NaN或Infinity,则会在编译阶段报错。所有其它情况的枚举成员被当作是需要计算得出的值。
常数枚举
当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。 常数枚举是在enum关键字前使用const修饰符。 常数枚举只能使用常数枚举表达式并且不同于常规的枚举的是它们在编译阶段会被删除。 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员。const enum Direction { //常数 Up, //0 Down = 3, //3 Left = Down + 4, //7 Right, //8 //计算的值 center = [1,2,3,4].length //error}let directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
编译结果:
var directions = [0 /* Up */, 3 /* Down */, 7 /* Left */, 8 /* Right */];
外部枚举
外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:declare enum Directions { Up, Down, Left, Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
之前提到过,declare 定义的类型只会用于编译时的检查,编译结果中会被删除。
上例的编译结果是:var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
外部枚举与声明语句一样,常出现在声明文件中。
同时使用 declare 和 const 也是可以的:declare const enum Directions { Up, Down, Left, Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
编译结果:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
Symbol也是值,但它不是字符串,也不是对象,而是是全新的——第七种类型的原始值。
从一个简单的布尔类型出发: 举个例子,假设你正在写一个JS库,可以通过CSS transitions使DOM元素在屏幕上移动。你可能会注意到,当你尝试在一个div元素上同时应用多重CSS transitions时并不会生效。实际效果是丑陋而又不连续的“跳闪”。你认为可以修复这个问题,但前提是你需要一种发现给定元素是否已经移动过的方 法。 应当如何解决这个问题呢? 一种方法是,用CSS API来告诉浏览器元素是否正在移动,但这样简直小题大做。在元素移动的第一时间内你的库就应该记录下移动的状态,所以它自然知道元素正在移动。 你真正想要的是一种持续跟踪某个元素正在移动的方法。你可以维护一个数组,记录所有正在移动的元素,每当你的库被调用来移动某个元素时,你可以检索数组来查看元素是否已经存在,亦即它是否正在移动中。 当然,如果数组非常大的话,线性搜索将会非常缓慢。 实际上你只想为元素设置一个标记:if (element.isMoving) { smoothAnimations(element); } element.isMoving = true;
这样也会有一些潜在的问题,事实上,你的代码很可能不是唯一一段操作DOM的代码。
if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) { smoothAnimations(element); } element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
这只会造成无畏的眼疲劳。
借助于密码学,你可以生成一个唯一的属性名称:// 获取1024个Unicode字符的无意义命名 var isMoving = SecureRandom.generateName(); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true;
object[name]
语法允许你使用几乎任何字符串作为属性名称。所以这个方法行之有效:冲突几乎是不可能的,并且你的代码看起来也很简洁。
但是这也将带来不良的调试体验。每当你在控制台输出(console.log()
)包含那个属性的元素时,你将会看到一堆巨大的字符串垃圾。假使你需要比这多得多的类似属性呢?你如何保持它们整齐划一?每当你重载的时候它们的命名甚至都不一样!
为什么这个问题如此困难?我们只想要一个小小的布尔值啊!
symbol是程序创建并且可以用作属性键的值,并且它能避免命名冲突的风险。
var mySymbol = Symbol();
调用Symbol()创建一个新的symbol,它的值与其它任何值皆不相等。
字符串或数字可以作为属性的键,symbol也可以,它不等同于任何字符串,因而这个以symbol为键的属性可以保证不与任何其它属性产生冲突。
obj[mySymbol] = "ok!"; // 保证不会冲突 console.log(obj[mySymbol]); // ok!
想要在上述讨论的场景中使用symbol,你可以这样做:
// 创建一个独一无二的symbol var isMoving = Symbol("isMoving");//使用Symbol来创建,其中引号内的内容被称作描述 ... if (element[isMoving]) {//如果有以Symbol为键isMoving为描述的属性 smoothAnimations(element); } element[isMoving] = true;
有关这段代码的一些解释:
到底什么是symbol?
> typeof Symbol() "symbol"
symbol被创建后就不可变更,你不能为它设置属性
每一个symbol都独一无二,不与其它symbol等同,即使二者有相同的描述也不相等symbol不能被自动转换为字符串,这和语言中的其它类型不同。 尝试拼接symbol与字符串将得到TypeError错误:> var sym = Symbol("<3"); > "your symbol is " + sym // TypeError: can't convert symbol to string > `your symbol is ${sym}` // TypeError: can't convert symbol to string
通过String(sym)
或sym.toString()
可以显示地将symbol转换为一个字符串,从而回避这个问题。
传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class
。
类的概念
这里对类相关的概念做一个简单的介绍:
new
生成Cat
和 Dog
都继承自 Animal
,但是分别实现了自己的 eat
方法。此时针对某一个实例,我们无需了解它是 Cat
还是 Dog
,就可以直接调用 eat
方法,程序会自动判断出来应该如何执行 eat
ES6 中类的用法
下面我们先回顾一下 ES6 中类的用法,更详细的介绍可以参考 。
属性和方法
使用 class
定义类,使用 constructor
定义构造函数。
class Animal { constructor(name) { this.name = name; } sayHi() { return `My name is ${this.name}`; }}let a = new Animal('Jack');console.log(a.sayHi()); // My name is Jack
类的继承
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。
class Cat extends Animal { constructor(name) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() }}
存取器
使用 getter 和 setter 可以改变属性的赋值和读取行为:
class Animal { constructor(name) { this.name = name; } get name() { return 'Jack'; } set name(value) { console.log('setter: ' + value); }}let a = new Animal('Kitty'); // setter: Kittya.name = 'Tom'; // setter: Tomconsole.log(a.name); // Jack
静态方法
使用 static
修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:
class Animal { static isAnimal(a) { return a instanceof Animal; }}let a = new Animal('Jack');Animal.isAnimal(a); // truea.isAnimal(a); // TypeError: a.isAnimal is not a function
ES7 中类的用法
ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。
实例属性
ES6 中实例的属性只能通过构造函数中的 this.xxx
来定义,ES7 提案中可以直接在类里面定义:
class Animal { name = 'Jack'; constructor() { // ... }}let a = new Animal();console.log(a.name); // Jack
静态属性
ES7 提案中,可以使用 static 定义一个静态属性:
class Animal { static num = 42; constructor() { // ... }}console.log(Animal.num); // 42
TypeScript 中类的用法
public private 和 protected
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public
、private
和 protected
。
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的private
修饰的属性或方法是私有的,不能在声明它的类的外部访问protected
修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的下面举一些例子:
class Animal { public name; public constructor(name) { this.name = name; }}let a = new Animal('Jack');console.log(a.name); // Jacka.name = 'Tom';console.log(a.name); // Tom
上面的例子中,name
被设置为了 public
,所以直接访问实例的 name
属性是允许的。
private
了: class Animal { private name; public constructor(name) { this.name = name; }}let a = new Animal('Jack');console.log(a.name); // errora.name = 'Tom'; // error
需要注意的是,TypeScript 编译之后的代码中,并没有限制 private
属性在外部的可访问性。
var Animal = (function () { function Animal(name) { this.name = name; } return Animal;}());var a = new Animal('Jack');console.log(a.name);a.name = 'Tom';
使用 private
修饰的属性或方法,在子类中也是不允许访问的:
class Animal { private name; public constructor(name) { this.name = name; }}class Cat extends Animal { constructor(name) { super(name); console.log(this.name); //error }}
而如果是用 protected
修饰,则允许在子类中访问:
class Animal { protected name; public constructor(name) { this.name = name; }}class Cat extends Animal { constructor(name) { super(name); console.log(this.name); }}
抽象类
abstract
用于定义抽象类和其中的抽象方法。
abstract class Animal { public name; public constructor(name) { this.name = name; } public abstract sayHi();}let a = new Animal('Jack'); //error
上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了。
其次,抽象类中的抽象方法必须被子类实现:abstract class Animal { public name; public constructor(name) { this.name = name; } public abstract sayHi();}//errorclass Cat extends Animal { public eat() { console.log(`${this.name} is eating.`); }}let cat = new Cat('Tom');
上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。
下面是一个正确使用抽象类的例子:abstract class Animal { public name; public constructor(name) { this.name = name; } public abstract sayHi();}class Cat extends Animal { public sayHi() { console.log(`Meow, My name is ${this.name}`); }}let cat = new Cat('Tom');
上面的例子中,我们实现了抽象方法 sayHi,编译通过了。
需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是:var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());};var Animal = (function () { function Animal(name) { this.name = name; } return Animal;}());var Cat = (function (_super) { __extends(Cat, _super); function Cat() { _super.apply(this, arguments); } Cat.prototype.sayHi = function () { console.log('Meow, My name is ' + this.name); }; return Cat;}(Animal));var cat = new Cat('Tom');
类的类型
给类加上 TypeScript 的类型很简单,与接口类似:
class Animal { name: string; constructor(name: string) { this.name = name; } sayHi(): string { return `My name is ${this.name}`; }}let a: Animal = new Animal('Jack');console.log(a.sayHi()); // My name is Jack
之前学习过,接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。
这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。类实现接口
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。
举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:
interface Alarm { alert();}class Door {}class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); }}class Car implements Alarm { alert() { console.log('Car alert'); }}
一个类可以实现多个接口:
interface Alarm { alert();}interface Light { lightOn(); lightOff();}class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); }}
上例中,Car
实现了 Alarm
和 Light
接口,既能报警,也能开关车灯。
接口继承接口
接口与接口之间可以是继承关系:
interface Alarm { alert();}interface LightableAlarm extends Alarm { lightOn(); lightOff();}
上例中,我们使用 extends
使 LightableAlarm
继承 Alarm
。
接口继承类
class Point { x: number; y: number;}interface Point3d extends Point { z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3};
混合类型
可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc { (source: string, subString: string): boolean;}let mySearch: SearchFunc;mySearch = function(source: string, subString: string) { return source.search(subString) !== -1;}
有时候,一个函数还可以有自己的属性和方法:
interface Counter { (start: number): string; interval: number; reset(): void;}function getCounter(): Counter { let counter =function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter;}let c = getCounter();c(10);c.reset();c.interval = 5.0;
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
简单的例子
首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
//只能返回string类型的数据function getData(value:string):string{ return value;}//同时返回 string类型 和number类型 (代码冗余)function getData1(value:string):string{ return value; }function getData2(value:number):number{ return value; }//同时返回 string类型 和number类型 any可以解决这个问题 function getData(value:any):any{ return '哈哈哈';}getData(123);getData('str');//传入的参数类型和返回的参数类型可以不一致function getData(value:any):any{ return '哈哈哈'; }
any
放弃了类型检查,它并没有准确的定义返回值的类型,如果想要传入什么 返回什么。比如:传入number 类型必须返回number类型 传入 string类型必须返回string类型,这时候,泛型就派上用场了。
//T表示泛型,具体什么类型是调用这个方法的时候决定的:function getData(value:T):T{ return value;}getData (123);getData ('1214231');getData ('2112'); /*错误的写法*/
泛型接口
可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc { (source: string, subString: string): boolean;}let mySearch: SearchFunc;mySearch = function(source: string, subString: string) { return source.search(subString) !== -1;}
当然也可以使用含有泛型的接口来定义函数的形状:
interface CreateArrayFunc {(length: number, value: T): Array ;}let createArray: CreateArrayFunc;createArray = function (length: number, value: T): Array { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}createArray(3, 'x'); // ['x', 'x', 'x']
进一步,我们可以把泛型参数提前到接口名上:
interface CreateArrayFunc{ (length: number, value: T): Array ;}let createArray: CreateArrayFunc ;createArray = function (length: number, value: T): Array { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}createArray(3, 'x'); // ['x', 'x', 'x']
注意,此时在使用泛型接口的时候,需要定义泛型的类型。
泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
class GenericNumber{ zeroValue: T; add: (x: T, y: T) => T;}let myGenericNumber = new GenericNumber ();myGenericNumber.zeroValue = 0;myGenericNumber.add = function(x, y) { return x + y; };
泛型参数的默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
function createArray(length: number, value: T): Array { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result;}
转载地址:http://kksef.baihongyu.com/