枚举是为了让程序可读性更好,比如用来描述用户的角色,普通的会员、付费的会员等,同时也限定了用户角色的种类,保证安全性,不会出现上帝角色这种乱入的东西。

枚举的类别与写法

默认值从0开始,依次递增,这个你应该还记得。

普通的枚举

  1. let str = 'something'
  2. enum test{
  3. test01,
  4. }
  5. enum FileAccess {
  6. None,
  7. Read = 1 << 1,
  8. Write = 1 << 2,
  9. ReadWrite = Read | Write,
  10. Test = test.test01,
  11. O = str.length
  12. }

对于前面这几种,是常理属性,因为它会直接计算出来,只要是使用了一元运算符号,或者使用了其他枚举的内容,都会计算出来。

  1. None,
  2. Read = 1 << 1,
  3. Write = 1 << 2,
  4. ReadWrite = Read | Write,
  5. Test = test.test01,

  1. O = str.length

则不会,编译出来的 JS 代码会是这个样子。

  1. var str = 'something';
  2. var test;
  3. (function (test) {
  4. test[test["test01"] = 0] = "test01";
  5. })(test || (test = {}));
  6. var FileAccess;
  7. (function (FileAccess) {
  8. FileAccess[FileAccess["None"] = 0] = "None";
  9. FileAccess[FileAccess["Read"] = 2] = "Read";
  10. FileAccess[FileAccess["Write"] = 4] = "Write";
  11. FileAccess[FileAccess["ReadWrite"] = 6] = "ReadWrite";
  12. FileAccess[FileAccess["Test01"] = 0] = "Test01";
  13. FileAccess[FileAccess["O"] = str.length] = "O";
  14. })(FileAccess || (FileAccess = {}));

它这个写法你可能看起来比较复杂。

你分开来看就行了。FileAccess 其实是一个对象。

  1. FileAccess["None"] = 0

此时 FileAccess.None 就为 0 了

枚举与类型 - 图1

我们可以看到给属性赋值的时候,返回的是这个值。

接着就是

  1. FileAccess[0] = "None"

把编译出来的代码加一个console.log然后放到 html中执行。

所以最后这个对象可能会是这样。

枚举与类型 - 图2

常量枚举

这个枚举只会存在ts文件中,编译成 js不会生成任何代码,全部都用它的值代替。

既然是常量枚举,那就在他前面加个 const 即可

  1. const enum Enum {
  2. A = 1,
  3. B = A * 2
  4. }
  5. var arr = [Enum.A, Enum.B]

它编译出来之后就这么一句。

var arr = [1 /* A */, 2 /* B */];

直接把它的值给编译了出来,只不过加了一个注释而已。

自动推导

往兼容性最好,描述最准确(且不会报错)的地方推

枚举与类型 - 图3

当我们给 a2 赋值为1的时候,编译器自动推导出为 number

枚举与类型 - 图4

这个被推导成了数字数组,因为number可以接受为 null

枚举与类型 - 图5

枚举与类型 - 图6

这个被推导成了对象数组,因为把 2 付给对象类型是不会报错的,从某种意义上说 2 也是 Number 对象的实例

枚举与类型 - 图7

而当我们给{},添加属性的时候,

枚举与类型 - 图8

它会添加一个|表示或者的关系。

  1. let a2 = 1;
  2. let arr2 = [1,23, null]
  3. let num: number = null
  4. let arr3 = [1,23, {a:2}]
  5. let obj : {} = 2

而对于类

枚举与类型 - 图9

你会发现 arr4 被推导成了a[]类型,为什么呢?

因为 ts 判断一个类是否相等是会去看它的类型,而这里的三个类的类型都是{}空对象。所以编译器认为3个都是相等的,所以就用了第一个的名字,也就是 a。

枚举与类型 - 图10

当我们打开 a 的属性的时候,我们发现 arr4 变成了 b[] 类型。

为什么呢?

a 的类型是什么呢?我们用内联的接口描述一下,{a:string}而 b 的类型是{},假如我们 arr4的类型是 a 的类型的数组的话,b 就不符合规范。

arr4的类型是 b 的类型数组,那就全部合适,不会报错。

其实此时我们是已经丢失了类型的。

枚举与类型 - 图11

此时的a,根本就拿不到任何它自己的属性。

枚举与类型 - 图12

跟使用它们的父类去接受值是一样的,丢失了自己的属性。

当然我们可以这样处理,强制转换。

枚举与类型 - 图13

我们把其他的注释也解开

枚举与类型 - 图14

此时的 arr4(a|b|c)[],表示一个可以放a/b/c实例化的数组。

所以我们取到的类型是枚举与类型 - 图15

依旧还是需要强制转换,那就没有什么其他的办法保证数组里面每一个类的特征吗?

有,利用元组,不过这样就不能把它看做数组,往里面添加新的东西了。

枚举与类型 - 图16

函数参数的自动推导

枚举与类型 - 图17

我们可以看到 此时的 ev 有代码提示,说明已经自动推导出了类型。

选择onkeyupf12,就可以看到以下声明,这是系统自带的类型。

  1. onkeyup: (this: Window, ev: KeyboardEvent) => any;

第一个参数是指定 this

枚举与类型 - 图18

这个this就像这样,可以提供代码提示。

这种绑定 this 的做法只能在 ClassInterface 里面使用。

ev就是我们函数的 ev 也就是KeyboardEvent类型,所以才有代码提示。

其实也就是从左边类型,推导出右边的类型。

类型之间的兼容性

之前我们提到过ts自动推导类型的时候,会推导出限制最低的类型。

而判断类型之间是否兼容,会通过类型的比较得出。

  1. interface Named {
  2. name: string;
  3. }
  4. class Person{
  5. name: string;
  6. }
  7. let p: Named;
  8. p = new Person();

这一段代码是正确没有报错的,我们用内联形式描述一下 Person {name:string},恰好与接口Named所描述的是一直的。

也就是说,其实 Person 隐含的意义实现了 Named 接口。

当我们给 Person 添加一个属性的时候依旧没报错。

  1. class Person{
  2. name: string;
  3. age: number;
  4. }

但是此时的属性 age丢失了,因为 Named上面并没有age

枚举与类型 - 图19

尽管我们知道,它一定有一个 age属性。

而此时为什么可以赋值,其实跟我们之前的数组保证类的特征类似。

此时 Person 的约束为{name:string;age:number}而 Named 的约束为{name:string}

你可以看成解构 {name} = {name,age} 所以此时的 age 就丢失了,尽管在 js 层面是一定存在 age 这个属性的,但是在 ts层面来说,因为你抛弃了age,所以它是不记得有 age这个东西的。

当然假如反过来{name,age} = {name} 就会报错,因为此时 age 拿不到任何值。

也就是说,约束少的可以得到约束多的一部分,也就是俩人都有的属性。

就像类型装换,number 可以转成 string,而并不是所有的 string都可以转成 number,只有存在一点不安全的东西,ts就不允许你这么做。

接下来我们看看函数之间的兼容。

枚举与类型 - 图20

我们可以看到把 x 赋值给 y 并没有报错,而当我们再次调用 y 的时候,依旧需要我们传递2个参数。但是在 js 层面来说,假如我们打印一下 y 的话。

枚举与类型 - 图21

这里为什么可以把一个少参数的函数赋给一个多的参数函数呢?你可能会这样问。

那么我问你,请问函数的参数越多是否意味着约束越强?

答案肯定是的,相比较 x 来说,y 的约束性更强,当把 x 复制给 y 的时候,首先会去判断 x 的参数类型与 y 的参数类型匹配不匹配。

这里的匹配是有顺序的。

枚举与类型 - 图22

当我们增加一个参数,依旧正常。

当我们修改一下位置,立马就报错了。

枚举与类型 - 图23

约束强的可以兼容约束弱的。

通过接口的兼容性来实现来实现 js 的一些特殊的回调方法。

  1. enum mEventType { Mouse, Keyboard }
  2. interface mEvent { timestamp: number; }
  3. interface mMouseEvent extends mEvent { x: number; y: number }
  4. interface mKeyEvent extends mEvent { keyCode: number }
  5. function listenEvent(eventType: mEventType, handler: (n: mEvent) => void) {}
  6. listenEvent(mEventType.Mouse, (e: mMouseEvent) => console.log(e.x + ',' + e.y));
  7. listenEvent(mEventType.Mouse, (e: mEvent) => console.log((<mMouseEvent>e).x + ',' + (<mMouseEvent>e).y));
  8. listenEvent(mEventType.Keyboard, <(e: mEvent) => void>((e: mKeyEvent) => console.log(e.keyCode)));

对于 mMouseEventmKeyEvent 来说, mEvent 相当于父类。

listenEvent(eventType: mEventType, handler: (n: mEvent) => void) {} 要求我们第二个参数传入的回调里面的一个参数是 mEvent 类型 因为 mMouseEventmKeyEvent都继承了mEvent,所有传入mMouseEventmKeyEvent 都是可以的。

当然上面的代码也用到了多次强制转换,一个是强制转换回调的类型,一个是强制转换参数的类型。

  1. class Animal {
  2. feet: number;
  3. constructor(name: string, numFeet: number) { }
  4. }
  5. class Size {
  6. feet: number;
  7. constructor(numFeet: number) { }
  8. }
  9. let a: Animal;
  10. let s: Size;
  11. a = new Animal('123', 1);
  12. s = new Size(2);
  13. a = s;

而对于类来说,只会比较他们之间的实例变量。

当泛型并没有实际约束任何属性的时候,他们是兼容的。

  1. interface Empty<T> {}
  2. let x: Empty<number>;
  3. let y: Empty<string>;
  4. x = y;

  1. interface Empty<T> {
  2. name: T;
  3. }

这样就不行了,因为 ts 会去检测属性是否匹配。

类型之间的逻辑

在之前,我们描述一类具有多个特征,可能要通过 implement 来实现多个接口,而现在我们可以&|来实现泛型、类型之间的逻辑。

比如 Person & Serializable & Loggable 意味着同时是 PersonSerializableLoggable

枚举与类型 - 图24

此时我们的 a 变量就具有了 a 接口和 b 接口的所以特征。

当然我们也可以等调用的时候再传

  1. interface a {
  2. name: string;
  3. }
  4. interface b {
  5. age: number;
  6. }
  7. let some = <T, U>(a: T & U) => {
  8. }
  9. some<a, b>({ name: '123', age: 28 })

枚举与类型 - 图25

不过这样就不会有任何的代码提示,除非你强制转换。

假如我们的函数想要传递数字或者是字符串,你可能会用 any 不过这样对其他程序员不友好,看函数的类型,并不能理解得到需要具体传啥。

  1. function some2(a : string | number) : any {
  2. if(typeof a === 'number') {
  3. a.toExponential()
  4. }
  5. }

所以我们可以通过|来实现或者逻辑。

而对于接口的逻辑|来说。

  1. interface Bird {
  2. fly();
  3. layEggs();
  4. }
  5. interface Fish {
  6. swim();
  7. layEggs();
  8. }
  9. function getSmallPet(): Fish | Bird {
  10. return { swim(){},layEggs(){} };
  11. }
  12. let pet = getSmallPet();
  13. if((pet as Fish).swim) {
  14. (pet as Fish).swim()
  15. }

枚举与类型 - 图26

这时候我们类型还是Fish | Bird

枚举与类型 - 图27

当我们去调用方法的时候,只能访问到他们共同的方法。

而想要判断是否某一类型,还是需要强转之后判断属性是否存在。

当然我们还可以写一个方法。

枚举与类型 - 图28

这里的代码提示是通过pet is Fish实现的,当然我们可以改成 boolean,尽管不会报错,但是这样我们就不会有代码提示了。

类型别名

你可以认为这种 {new(name:string):Person;hello():void}为内联类型描述,而 interface,认为 class 而类型别名,就好像把多个 class 或者内联的组合在一起。

当然类型别名是不再支持继承的,当你没法用接口描述类型的时候,你就应该尝试这种方式了。

  1. interface a {
  2. name: string;
  3. }
  4. interface b {
  5. age: number;
  6. }
  7. type aAndB = a & b & {sayHello(name: string)};
  8. function some3(a : aAndB) {
  9. a.age;
  10. a.name;
  11. a.sayHello('bob');
  12. }

当然,你也可以给出具体的值,这样就有点类似枚举,也就是说,传入的值,必须是其中的某一项。

  1. type Easing = "ease-in" | "ease-out" | "ease-in-out";

而对于类型来说,还有一种特殊的类型 this,这是为方便实现实现链式调用。

  1. class BasicCalculator {
  2. public constructor(protected value: number = 0) { }
  3. public currentValue(): number {
  4. return this.value;
  5. }
  6. public add(operand: number): this {
  7. this.value += operand;
  8. return this;
  9. }
  10. public multiply(operand: number): this {
  11. this.value *= operand;
  12. return this;
  13. }
  14. // ... other operations go here ...
  15. }
  16. let v = new BasicCalculator(2)
  17. .multiply(5)
  18. .add(1)
  19. .currentValue();

就像这个 add 方法。

  1. public add(operand: number): this {
  2. this.value += operand;
  3. return this;
  4. }

实现链式调用的密码就是返回 this