# 枚举与类型

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

## 枚举的类别与写法

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

### 普通的枚举

```
let str = 'something'

enum test{
    test01,
}

enum FileAccess {
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    Test = test.test01,
    O = str.length
}
```

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

```
None,
Read    = 1 << 1,
Write   = 1 << 2,
ReadWrite  = Read | Write,
Test = test.test01,
```

而

```
O = str.length
```

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

```
var str = 'something';
var test;
(function (test) {
    test[test["test01"] = 0] = "test01";
})(test || (test = {}));
var FileAccess;
(function (FileAccess) {
    FileAccess[FileAccess["None"] = 0] = "None";
    FileAccess[FileAccess["Read"] = 2] = "Read";
    FileAccess[FileAccess["Write"] = 4] = "Write";
    FileAccess[FileAccess["ReadWrite"] = 6] = "ReadWrite";
    FileAccess[FileAccess["Test01"] = 0] = "Test01";
    FileAccess[FileAccess["O"] = str.length] = "O";
})(FileAccess || (FileAccess = {}));
```

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

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

```
FileAccess["None"] = 0
```

此时 `FileAccess.None` 就为 0 了

![](/files/-LB9AJCerrab0CS54kND)

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

接着就是

```
FileAccess[0] = "None"
```

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

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

![](/files/-LB9AJF8mDtBfD2EwmJ0)

### 常量枚举

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

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

```
const enum Enum {
    A = 1,
    B = A * 2
}

var arr = [Enum.A, Enum.B]
```

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

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

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

## 自动推导

往兼容性最好，描述最准确（且不会报错）的地方推

![](/files/-LB9AJMe_k-C9-s-Lh3i)

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

![](/files/-LB9AJQChof-OnoYpqkY)

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

![](/files/-LB9AJSr7DqLiuiw9cit)

![](/files/-LB9AJUlfNiIsDP3msYK)

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

![](/files/-LB9AJZ245MCD9ohbhQG)

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

![](/files/-LB9AJcXVEJ5RSl5zRYe)

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

```
let a2 = 1;

let arr2 = [1,23, null]

let num: number = null

let arr3 = [1,23, {a:2}]

let obj : {} = 2
```

而对于类

![](/files/-LB9AJicuiwbnVIDiZSN)

你会发现 `arr4` 被推导成了`a[]`类型，为什么呢？

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

![](/files/-LB9AJnGoaIoADw4taqK)

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

为什么呢？

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

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

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

![](/files/-LB9AJuNrayqm7mfWTdz)

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

![](/files/-LB9AJzMBKnzLCPK-73F)

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

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

![](/files/-LB9AK1mSFA2CrVYLDMd)

我们把其他的注释也解开

![](/files/-LB9AK50ccMzZC3wBlBs)

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

所以我们取到的类型是 ![](/files/-LB9AKBWDagjaNwAN87z)

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

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

![](/files/-LB9AKD6nIR6YRBusPJD)

函数参数的自动推导

![](/files/-LB9AKI4zIdunNLco8OC)

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

选择`onkeyup`按 `f12`，就可以看到以下声明，这是系统自带的类型。

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

第一个参数是指定 `this`。

![](/files/-LB9AKQ6jXgU2u8sQUgs)

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

这种绑定 `this` 的做法只能在 `Class` 和 `Interface` 里面使用。

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

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

## 类型之间的兼容性

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

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

```
interface Named {
    name: string;
}

class Person{
    name: string;
}

let p: Named;
p = new Person();
```

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

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

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

```
class Person{
    name: string;
    age: number;
}
```

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

![](/files/-LB9AKpYpnEHyTqzIF7v)

尽管我们知道，它一定有一个 `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`就不允许你这么做。

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

![](/files/-LB9ALAQFPXszhvLyrB3)

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

![](/files/-LB9ALGbgtqPDqINIH-5)

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

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

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

这里的匹配是有顺序的。

![](/files/-LB9ALLslMU-qjPud9dl)

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

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

![](/files/-LB9ALQ8QJuZBe1XmVIx)

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

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

```
enum mEventType { Mouse, Keyboard }

interface mEvent { timestamp: number; }
interface mMouseEvent extends mEvent { x: number; y: number }
interface mKeyEvent extends mEvent { keyCode: number }

function listenEvent(eventType: mEventType, handler: (n: mEvent) => void) {}

listenEvent(mEventType.Mouse, (e: mMouseEvent) => console.log(e.x + ',' + e.y));

listenEvent(mEventType.Mouse, (e: mEvent) => console.log((<mMouseEvent>e).x + ',' + (<mMouseEvent>e).y));
listenEvent(mEventType.Keyboard, <(e: mEvent) => void>((e: mKeyEvent) => console.log(e.keyCode)));
```

对于 `mMouseEvent` 和 `mKeyEvent` 来说， `mEvent` 相当于父类。

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

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

```
class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = new Animal('123', 1);

s = new Size(2);

a = s;
```

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

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

```
interface Empty<T> {}

let x: Empty<number>;
let y: Empty<string>;

x = y;
```

而

```
interface Empty<T> {
    name: T;
}
```

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

## 类型之间的逻辑

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

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

![](/files/-LB9AMBFZ1lIS3-Bp6im)

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

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

```
interface a {
    name: string;
}

interface b {
    age: number;
}

let some = <T, U>(a: T & U) => {

}

some<a, b>({ name: '123', age: 28 })
```

![](/files/-LB9AMQ7ePNmkk0DwOLT)

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

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

```
function some2(a : string | number) : any {
    if(typeof a === 'number') {
        a.toExponential()
    }
}
```

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

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

```
interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    return { swim(){},layEggs(){} };
}

let pet = getSmallPet();

if((pet as Fish).swim) {
    (pet as Fish).swim()
}
```

![](/files/-LB9AMu3_GB8yGvXUkwv)

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

![](/files/-LB9AN6_d5PmPfN6G-gP)

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

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

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

![](/files/-LB9ANF0O9Igj_DvXSSN)

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

## 类型别名

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

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

```
interface a {
    name: string;
}

interface b {
    age: number;
}

type aAndB = a & b & {sayHello(name: string)};

function some3(a : aAndB) {
     a.age;
     a.name;
     a.sayHello('bob');
}
```

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

```
type Easing = "ease-in" | "ease-out" | "ease-in-out";
```

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

```
class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();
```

就像这个 `add` 方法。

```
public add(operand: number): this {
   this.value += operand;
   return this;
}
```

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://nodelover.gitbook.io/typescript/mei-ju-yu-lei-xing.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
