4-1. 字面量类型的问题

前言

任务列表

  • 理解使用字面量类型来约束变量的取值范围所导致的一些问题
    • 重复代码问题 - 可以使用 type 类型别名来解决
    • 逻辑含义和真实值混淆的问题 - 没法解决
    • 编译结果中不含字面量类型约束信息,在编译结果中无法获取到取值范围中可能都有哪些值 - 没法解决

认识某些场景下,若我们要限定变量的取值范围时,使用先前所学习的知识点来实现所导致的一系列问题。

notes

扩展类型

ts 给我们提供的基本类型不足以满足我们的实际需求,我们需要基于 ts 给我们提供的基本类型进行扩展。

扩展类型就是我们自己定义的一些类型:

  • 类型别名
  • 枚举
  • 接口

:::tips 枚举将在本章进行介绍;
类型别名在 3. 基本类型检查 介绍过;
接口和类也都将在后续课程中逐一介绍到; :::

我们本章节要学习的“枚举”,通常用于约束某个变量的取值范围。

虽然,我们前面所学习的“字面量”和“联合类型”配合使用,也可以达到同样的效果。但是,它们还存在一些问题,这些问题就是本节课要介绍的点。

字面量类型的问题

  1. 在类型约束位置,会产生重复代码。
  2. 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
  3. 字面量类型不会进入到编译结果。

对于有关重复代码的问题,可以使用类型别名来解决。但是,另外两个问题,使用类型别名就没法解决了。

上面提到的这些问题,都将在 codes 部分,以代码的形式来呈现。问题具体如何解决,会在 3-2 中介绍。

codes

重复代码的问题

  1. let gender: "male" | "female";
  2. gender = "male";
  3. function searchUsers(g: "male" | "female") {}

使用字面量类型,会在类型约束的位置产生大量的重复代码。对于该问题,我们可以使用之前学习到的类型别名来解决。

type Gender = "male" | "female";

let gender: Gender;

gender = "male";

function searchUsers(g: Gender) {}

使用类型别名解决在类型约束位置产生的重复代码问题。

逻辑含义和真实值的问题

我们知道,对于性别,无非就是“男 | 女”。但是,我们显示给用户看的真实的值,可就不一定是“男 | 女”了,也可以是“先生 | 女士”、“帅哥 | 美女”、“boy | girl”、“male | female”等等。

那么就会有这样一种情况,倘若有一天,我们需要修改展示给用户看的值,这样我们需要修改的地方就会很多,工作量就会很大,而且容易出错。而我们希望的是,只要修改类型约束部分,修改一个地方就可以完成所有的修改。

对于这样的需求,使用原来的字面量式写法,显然就无法解决了。但是若使用枚举,就可以很好地解决这样的问题。

type Gender = "男" | "女";

let gender: Gender;

gender = "男";

function searchUsers(g: Gender) {}

image.png

当我们修改完 1 后,还得修改 2。

虽然在个案例中看不出有什么大问题,但是试想一下,若程序的规模很大,很多地方都用到了 Gender 类型,那我们需要修改的量,可就不是上边这么一丢丢了。

编译结果

let gender;
gender = "男";
function searchUsers(g) { }

从编译结果中,我们可以看出:类型别名是不会生成到最终的结果中的。

若我们在编辑结果中想要获取到 gender 所有可能的值,也就是 Gender 中限定的值,那么使用上面这种类型别名的写法,也是无法获取到的。(该需求也可以使用「枚举」来解决)

4-2. 枚举的使用

前言

任务列表

  • 掌握枚举的语法
  • 理解枚举的编译结果
  • 理解数字枚举的特点
  • 掌握枚举的最佳实践

notes

只要是限定变量的取值范围,我们都可以使用枚举来实现。

本节学习约束某个变量的取值范围的最佳实践 - 枚举。

enum 枚举名 {
  枚举字段1 = 值1,
  枚举字段2 = 值2,
  ...
}
  • 枚举会出现在编译结果中,编译结果中表现为一个对象;
  • 枚举的字段值可以是字符串或数字;
  • 数字枚举的值会自动自增;
  • 被数字枚举约束的变量,可以直接赋值为数字;
  • 数字枚举的编译结果(双向映射 k <-> v)和字符串枚举的编译结果(单向映射 k -> v)有差异;

:::tips 至于为什么数字枚举允许我们直接赋值为数字,想要了解的话,可以看看本章的最后一节4-4. 扩展知识:枚举的位运算 :::

最佳实践

  1. 尽量不要在一个枚举中,即出现字符串字段,又出现数字字段;
  2. 使用枚举式,尽量使用枚举字段的名称,而不使用真实的值;

codes

字符串枚举

enum Gender { // 枚举的写法
  male = "男",
  female = "女",
}

let gender: Gender;

gender = Gender.male;

function searchUsers(g: Gender) {}

male 和 female 是逻辑名称,它们映射的值,是真实展示给用户看的值。

我们在写的时候,写的是逻辑名称,比如:Gender.male

但是读取到的,是真实的值。

认识一下枚举的图标:

image.png

一键重命名:

我们通常遇到的需求,修改的都是展示给用户看的值,一般不会有需求是改变逻辑含义的。

但是,若我们需要修改逻辑名,也是可以一键修改的,在指定的逻辑名上右键,选择重命名符号即可修改。(或直接按 F2)

image.png

编译结果分析:

在命令行输入 tsc 命令对 1.ts 进行编译,生成的编译结果文件如下:

var Gender;
(function (Gender) {
    Gender["Male"] = "\u7537";
    Gender["female"] = "\u5973";
})(Gender || (Gender = {}));
let gender;
gender = Gender.Male;
function searchUsers(g) { }

编码

由于我们使用到了中文,会进行 Unicode 编码:

  • “男” 对应的 Unicode 编码是 \u7537
  • “女” 对应的 Unicode 编码是 \u5973

在编译结果中,枚举会被识别为一个对象。

// 该表达式是一个函数
(function (Gender) {
    Gender["Male"] = "\u7537";
    Gender["female"] = "\u5973";
})
(function (Gender) {
    Gender["Male"] = "\u7537";
    Gender["female"] = "\u5973";
})(Gender || (Gender = {}));

这么写相当于调用上一步创建的函数,并传入参数:Gender || (Gender = {})

参数分析:

该参数也是一个表达式,该表达式的返回值得看 Gender 的值,若 Gender 的值是空,那么会执行 Gender = {}表达式,该赋值表达式执行后,Gender 被赋值为一个空对象 {}。由于赋值表达式的返回值就是所赋的值,所以该表达式会返回该对象。

var Gender {
  Male: "男",
  female: "女"
}

从编译结果中获取约束条件范围内的所有可能的取值:

enum Gender {
  Male = "男",
  female = "女",
}

function printGenders() {
  const vals = Object.values(Gender);
  vals.forEach((v) => console.log(v));
}
printGenders();

在编译结果中,我们也可以获取到 Gender 中定义的真实值。

数字枚举

枚举的字段值可以是字符串或数字,如果字段值是数字的话,那么就是数字枚举。

enum Level {
  level1 = 1,
  level2 = 2,
  level3 = 3,
}

let l: Level = Level.level1;
l = Level.level2;

对比数字枚举和字符串枚举的编译结果差异:

var Level;
(function (Level) {
    Level[Level["level1"] = 1] = "level1";
    Level[Level["level2"] = 2] = "level2";
    Level[Level["level3"] = 3] = "level3";
})(Level || (Level = {}));
let l = Level.level1;
l = Level.level2;

数字枚举的编译结果和字符串枚举是有差异的,可以理解为:

  1. 字符串枚举:单向映射 key -> value
  2. 数字枚举:双向映射 key <-> value
(function (Level) {
    Level[Level["level1"] = 1] = "level1";
    Level[Level["level2"] = 2] = "level2";
    Level[Level["level3"] = 3] = "level3";
})(Level || (Level = {}));
// 相当于生成了下面这样一个对象
{
  level1: 1,
  level2: 2,
  level3: 3,
  "1": "level1",
  "2": "level2",
  "3": "level3",
}

数字枚举默认自增:

enum Level {
  level1 = 1,
  level2, // 相当于写了 level2 = 2,
  level3, // 相当于写了 level3 = 3,
}

let l: Level = Level.level1;
l = Level.level2;

数字枚举的值会自动自增;

这种 4.ts 中的代码,和 3.ts 中的代码,是完全等效的。

枚举字段值都为空:

若枚举字段的值,都没有填写,那么默认是数字枚举,并且起始字段的值为 0。

enum Level {
  level1,
  level2,
  level3,
}

// 等效写法

enum Level {
  level1 = 0,
  level2 = 1,
  level3 = 2,
}

被数字枚举约束的变量,我们可以直接将真实值赋给它(但是不推荐这么做)

enum Level {
  level1,
  level2,
  level3,
}

let lev: Level = 1; // 不会报错
lev = 2; // 不会报错

被数字枚举约束的变量,可以直接赋值为数字。但是,不推荐这么写,这么写意味着我们又在直接使用真实值了。

若要按照上述写法来写,那么用之前学习的内容就完全可以实现了,这让枚举失去了意义。

type Level = 0 | 1 | 2;
let lev: Level = 1;
lev = 2;

4-3. 练习:使用枚举优化扑克牌程序

前言

优化步骤:

  1. 使用枚举列出扑克牌花色的所有可能
  2. 使用枚举列出扑克牌的数字范围
  3. 修改后续相关逻辑

    codes

袁进代码

type Color = "♥" | "♠" | "♦" | "♣"; // 扑克花色
type NormalCard = {
  // 一张牌
  color: Color;
  mark: number;
};
type Deck = NormalCard[]; // 一副牌

function createDeck(): Deck {
  const deck: Deck = [];
  for (let i = 1; i <= 13; i++) {
    deck.push(
      {
        mark: i,
        color: "♠",
      },
      {
        mark: i,
        color: "♣",
      },
      {
        mark: i,
        color: "♥",
      },
      {
        mark: i,
        color: "♦",
      }
    );
  }
  return deck;
}

function printDeck(deck: Deck) {
  let result = "";
  deck.sort(() => Math.random() - 0.5); // 乱序
  deck.forEach((pocker, i) => {
    const num = pocker.mark;
    if (num <= 10) {
      result += num + pocker.color + " ";
    } else if (num === 11) {
      result += "J" + pocker.color + " ";
    } else if (num === 12) {
      result += "Q" + pocker.color + " ";
    } else if (num === 13) {
      result += "K" + pocker.color + " ";
    }
    // 每打印 17 张牌,换行。
    if ((i + 1) % 17 === 0) {
      result += "\n";
    }
  });
  console.log(result);
}

const deck = createDeck();
printDeck(deck);
enum Color {
  heart = "♥",
  spade = "♠",
  club = "♣",
  diamond = "♦",
}

enum Mark {
  A = "A",
  two = "2",
  three = "3",
  four = "4",
  five = "5",
  six = "6",
  seven = "7",
  eight = "8",
  nine = "9",
  ten = "10",
  jack = "J",
  queen = "Q",
  king = "K",
}

type NormalCard = {
  // 一张牌
  color: Color;
  mark: Mark;
};

type Deck = NormalCard[]; // 一副牌

function createDeck(): Deck {
  const deck: Deck = [];
  const marks = Object.keys(Mark);
  const colors = Object.keys(Color);
  for (const m of marks) {
    for (const c of colors) {
      deck.push({
        mark: Mark[m],
        color: Color[c],
      });
    }
  }
  return deck;
}

function printDeck(deck: Deck) {
  let result = "";
  deck.sort(() => Math.random() - 0.5); // 乱序
  deck.forEach((pocker, i) => {
    result += pocker.color + pocker.mark + " ";
    if ((i + 1) % 17 === 0) {
      result += "\n";
    }
  });
  console.log(result);
}

const deck = createDeck();
printDeck(deck);

优化前后的对比:主要优化了一副扑克牌的创建流程

对于枚举,我们直接将其视作一个对象来处理即可。从枚举的编译结果中可以得知枚举的编译结果就是对象。

我的代码

type Colors = "❤" | "♠" | "♣" | "◇";

type Poker = {
  // 一张牌
  number: number | string;
  color?: Colors;
};

type Deck = Poker[]; // 一副牌

// 创建一副牌
function createDeck() {
  const deck: Poker[] = [];
  // 插入 A ~ K
  for (let i = 1; i <= 13; i++) {
    let number: number | string;
    if (i === 1) number = "A";
    if (i === 11) number = "J";
    if (i === 12) number = "Q";
    if (i === 13) number = "K";
    deck.push({
      number: i,
      color: "❤",
    });
    deck.push({
      number: i,
      color: "♠",
    });
    deck.push({
      number: i,
      color: "♣",
    });
    deck.push({
      number: i,
      color: "◇",
    });
  }
  // 插入大小王
  deck.push({
    number: "joker",
  });
  // 插入大王
  deck.push({
    number: "JOKER",
  });
  deck.sort(() => Math.random() - 0.5); // 打乱牌序
  return deck;
}

// 打印一副扑克
function printDeck(deck: Deck) {
  let result = "底牌:";
  const desk = deck.splice(0, 3); // 底牌
  desk.forEach((poker, i) => {
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  result += '\n用户1:'
  deck.forEach((poker, i) => {
    if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  console.log(result);
}

const deck = createDeck();

printDeck(deck);
// 黑桃(Spade)、红桃(Heart)、方块(Diamond)、梅花(Club)
enum Colors { // 花色
  Heart = "❤",
  Spade = "♠",
  Club = "♣",
  Diamond = "◇",
}

// one,two,three,four,five,six,seven,eight,nine,ten,eleven,twelve,thirteen
// 牌编号
enum Nums {
  one = "A",
  two = "2",
  three = "3",
  four = "4",
  five = "5",
  six = "6",
  seven = "7",
  eight = "8",
  nine = "9",
  ten = "10",
  eleven = "J",
  twelve = "Q",
  thirteen = "K",
}

type Poker = {
  // 一张牌
  number: Nums | "JOKER" | "joker";
  color?: Colors;
};

type Deck = Poker[]; // 一副牌

// 创建一副牌
function createDeck() {
  const deck: Deck = [];
  // 插入大小王
  deck.push({
    number: "joker",
  });
  // 插入大王
  deck.push({
    number: "JOKER",
  });
  // 插入 A ~ K
  const colorKeys = Object.keys(Colors);
  const numKeys = Object.keys(Nums);
  for (let i = 0; i < colorKeys.length; i++) {
    for (let j = 0; j < numKeys.length; j++) {
      deck.push({
        number: Nums[numKeys[j]],
        color: Colors[colorKeys[i]]
      });
    }
  }
  deck.sort(() => Math.random() - 0.5); // 打乱牌序
  return deck;
}

// 打印一副扑克
function printDeck(deck: Deck) {
  let result = "底牌:";
  const desk = deck.splice(0, 3); // 底牌
  desk.forEach((poker, i) => {
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  result += '\n用户1:'
  deck.forEach((poker, i) => {
    if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  console.log(result);
}

const deck = createDeck();

printDeck(deck);

export {};

image.png

4-4. 扩展知识:枚举的位运算

前言

扩展内容暂且不看。。。