本章节介绍的所有内容,都是可选的。也就是说,我们在 ts 中,可以选择用、或者不用。虽然知识量可能会有些多,但是都非常简单,容易理解,不要有压力。

3-1. 类型约束和编译结果对比

前言

任务列表

  • 一键重命名符号
  • 一键转到定义
  • 类型推导
  • 知道如何识别类型推导是否成功

本节课主要介绍下面两个点:

  1. ts 如何进行类型约束
  2. 源代码和编译结果的差异

notes

类型约束

一般仅会对以下内容进行约束:

  • 变量
  • 函数参数
  • 函数返回值

通常,我们只要对函数参数和变量进行约束即可。函数的返回值会根据我们对函数参数、变量的约束,自行推导(类型推导)出来。

类型约束的语法::类型

  1. let nickName:string;
  2. nickName = "xxx";
  3. // nickName = 123; // => 会报错

若我们给变量的值和类型约束所约束的类型有冲突,那么会立刻出现 ❌ 提示。

function sum(a:number, b:number):number {
  return a + b;
}
function sum(a:number, b:number):number {
  return a + b;
}

sum = 1; // ❎
// 如果我们采用这种方式,尝试给 sum 函数重新赋值:
// 在 js 中,这种做法是允许的
// 在 ts 中,这种做法会报错

ts 是可选的、静态的类型系统。静态的特点是,类型会在编译时就确定下来,如果在编译之前,被编译的 ts 文件出现了错误,那么将无法进行编译。

类型推导

类型推导:根据我们所写的代码,自动完成类型检查。

function sum(a:number, b:number) {
  return a + b;
}

对于上面那种写法,我们可以省略掉对函数返回值的类型约束。

因为根据我们对参数 a、b 的约束,它们都是数字,返回的是它们相加的结果,必然也是一个数字。

ts 可以识别出返回值的类型必然是一个数字,所以我们可以省略对函数返回值的类型约束 => 类型推导。

image.png

image.png

我们在使用 sum 的返回值给变量 result 进行赋值的时候,也可以省略掉对变量 result 的类型约束。因为 ts 能够推导出 sum 函数的返回结果就是 number 类型,那么 result 自然也就是 number 类型啦。

// 👇🏻 两种写法是等效的
let phone:string = "11122223333";
let phone = "11122223333";

image.png

我们声明了一个名为 phone 的变量,并同时给它赋值为一个字符串,此时类型推导就能推断变量 phone 应该是一个字符串类型。

这种在声明变量的同时对其进行赋值的做法是很常见的。可见,很多时候我们并不需要刻意去对变量的类型进行约束,ts 会自动帮我们推导出它的数据类型。

即便一开始没有赋初始值,类型推导没法帮我们完成类型约束,我们手动进行类型约束的成本也不会太高。通常我们在定义变量、编写函数时,都知道该变量或该函数的参数、返回值应该是啥类型。这些时候就应该给它们添加上类型约束。

类型推导发生了很多地方,只要能根据现有的条件推导出类型,ts 一般都能帮我们自动完成类型推导。比如上图中的 result 变量,推导出的结果将会是 number 类型。因为 sum 函数的返回值推导出的结果是 number 类型,又将 sum 函数的返回值赋值给 result。所以我们写的 const result = sum(1, 2)其实就等效于 const result: number = sum(1, 2)

一键重命名、一键转到定义

  • F2:一次性重命名
  • F12:快速跳转到函数的声明位置

image.png

:::warning 测试的时候发现了一些问题,简单记录一下:

如果使用的是 es6 规范,可以跨文件实现一键重命名、一键转到定义
如果使用的是 commonjs 规范,那么跨文件实现一键重命名、一键转到定义都不好使

并且在使用模块化语句的时候,一直报错,目前还不知道原因,等学到模块化相关的内容之后,再重点看看。

如果直接在 js 中使用 es6 module 来写,也可以实现上述所记录的「一键重命名」、「一键转到定义」的效果。 :::

识别某个变量是否添加了类型约束

在编辑器 vscode 中判断是否有类型约束的方式:在 vscode 编辑器中,看变量左下角是否有三个点,若有三个小点,表示没有进行类型约束,不知道是啥类型,它可以是任意类型 any。

image.png

上面这种写法,类型推导也推导不出具体是啥类型,最终结果只能是 any 类型。

any 表示任意类型,对该类型,ts 不进行类型检查。

编译结果对比

执行命令 tsc,对比编译后生成的 js 文件和我们写的 ts 文件的差异:

image.png

会发现只有类型约束没了,其他的内容完全没变。

Q & A

🤔 电话号码、身份证,应该约束为 number 还是 string? 答:string 类型

识别方式:如果我们是按照个十百千这种方式来读,那么应该是 number,否则应该是 string

以该电话号码为例:15157722155

如果我们将其读作:么五么五七七二二么五五,那么应该将其视作 string 类型
如果我们将其读作:一百五十一亿五千七百七十二万两千一百五十五,那么应该是 number 类型

同理:学号、工号,等等。都应该是字符串类型。

3-2. 基本类型

前言

任务列表

  • strickNullChecks
  • 掌握基本类型约束的语法
    • 数字 :number
    • 字符串 :string
    • 布尔值 :boolean
    • 对象 :object
    • 数组 :number[]:Array<number>
    • :null
    • 未定义 :undefine

参考资料
compilerOptions.strictNullChecks 开启更加严格的空类型检查。

notes

基本类型

  • 数字 :number
  • 字符串 :string
  • 布尔值 :boolean
  • 对象 :object
  • 数组 :number[]:Array<number>
  • :null
  • 未定义 :undefine

nullundefined 是所有其他类型的子类型,它们可以赋值给其他类型。

通过添加 strictNullChecks:true配置,可以获得更严格的空类型检查,nullundefined 只能赋值给自身。

codes

function isOdd(n: number): boolean { }

封装 boolean 函数,当我们写到这一步时,boolean 会有警告。因为我们还没有开始写函数体,没有指定函数的返回值。

若函数的返回值的约束类型不为 “void” 或 “any”,那么该函数必须要写 return 指定返回值。

function isOdd(n: number): boolean {
  return n % 2 === 0;
}

当我们写到这一步时,就不会有报错了

function isOdd(n: number) {
  return n % 2 === 0;
}

返回值也可以不进行约束,ts 会智能地进行类型推导,它能够识别出表达式 n % 2 === 0 的结果必然是一个 boolean 类型。

image.png

如上图所示,ts 会自动识别出函数 isOdd 的返回值是 boolean 类型,所以我们可以直接省略对其返回值的类型声明。

可见,在很多情况下,ts 都能够帮我们完成类型推导。类型推导是否成功,在前面介绍过,就是看变量名左下角是否有三个小点。类型推导的结果,我们也可以查看,方式也很简单,直接将鼠标悬停在变量名上边即可。

let nums_1: number[] = [1, 2, 3];

let nums_2: Array<number> = [1, 2, 3];

number[]Array<number> 的语法糖,它们俩是等效的。

表示的含义是:要求数组的每一项都是数字类型。

:::warning 为了防止在写 react 时,和 jsx 的语法发生冲突,推荐使用第一种写法:number[] :::

let nums = [1, 2, 3];

我们也可以直接这么写,ts 会帮我们完成类型推导。将鼠标悬停在 nums 上,会提示 let nums: number[],表示 nums 是一个数字数组,nums 的每一个成员都是数字类型。

image.png

所以说,我们平时在写 ts 时,虽然还是用原来写 js 的习惯来写,但是代码的质量就会比之前要高很多,它能帮我们规避一些不必要的问题。

let user: object = {
  name: "dahuyou",
  age: 22,
};

object 虽然也具有一定的限制作用,但是它的限制作用不强。它只能限制我们给 user 变量赋值时必须赋一个对象,但是无法限制对象里面每个成员具体是什么类型。所以我们一般很少会使用它。

function printObj(obj: object) {
  const vals = Object.values(obj);
  vals.forEach((v) => console.log(v));
}

printObj({
  name: "dahuyou",
  age: 22,
});

需求:封装一个 printObj 函数,该函数传入一个对象作为参数,要求答应出传入的对象身上的所有属性值。

let str: string = undefined;

nullundefined 是所有其他类型的子类型,它们可以赋值给其他类型。

上面这种写法,并不会报错,因为 undefined 会被识别为 string 的子类型,上面这种赋值的方式是被允许的。

但是这样的操作往往是我们不希望看到的,我们可以通过在配置中添加 strictNullChecks: true,以获得更严格的空类型检查,使得 nullundefined 只能赋值给自身。

在开启 "strictNullChecks": true 的情况下,会报错:不能将类型“undefined”分配给类型“string”。

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "lib": [
      "es2016"
    ],
    "outDir": "./dist",
    "strictNullChecks": true
  },
  "include": [
    "./src"
  ],
}

strictNullChecks

默认值是 false,表示 nullundefined 是可以被赋值给其它类型的,并不会报错;

如果将其设置为 true,表示开启更加严格的空类型检查,此时 nullundefined 将无法再赋值给其它类型;

3-3. 其它类型

前言

任务列表

  • 掌握以下类型
    • 联合类型
    • viod 类型
    • never 类型
    • 字面量类型
    • 元祖类型(Tuple)
    • any 类型
  • 理解「类型保护」
    • 掌握如何使用 typeof 关键字在判断语句块中触发类型保护

notes

其它类型

除了上节介绍的基本类型外,ts 还给我们提供了一些其它常用的类型:

  • 联合类型:多种类型任选其一;
  • viod 类型:通常用于约束函数的返回值,表示函数没有任何返回;
  • never 类型:通常用于约束函数的返回值,表示该函数永远不可能结束;
  • 字面量类型:使用一个值进行约束;
  • 元祖类型(Tuple):一个固定长度的数组,并且数组中的每一项的类型确定;
  • any 类型:any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型;

类型保护

当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型。

联合类型可以配合类型保护进行判断。

类型保护的触发方式有很多种,后边会有对应的课程专门介绍,通常我们会使用 typeof 来触发基本类型(number、string、boolean 等)的类型保护。

codes

联合类型

let foo: string | number[] = [1, 2, 3];
// foo 可以是 string 类型,也可以是 number[] 类型。

因为 foo 是一个联合类型,它的类型此时是没法确定的,所以当我们想要去调用变量 foo 身上的 api 时,ide 是没法给我们提供准确的智能提示的。

类型保护

function test(x: number[] | string) {
  let result: string;
  x; // => (parameter) x: string | number[]

  // 类型保护
  if (typeof x === "string") {
    x; // => (parameter) x: string
    result = x.toUpperCase();
  } else {
    x; // => (parameter) x: number[]
    result = x.toString();
  }

  return result;
}

console.log(test("abc")); // => ABC
console.log(test([1, 2, 3])); // => 1,2,3

联合类型配合类型保护,可以明确变量 x 具体是什么类型。

当 ts 能够明确某个变量具体是什么类型时,我们在调用它身上的一些方法时,都会有精准的智能提示。

image.png

类型保护,是指通过某种手段,在判断语句块中确定一个变量的具体类型。

let test: string | number | undefined;

if (typeof test === "string") {
  console.log("test is string");
} else if (typeof test === "number") {
  console.log("test is number");
} else {
  console.log("test is undefined");
}

补充一个小细节:如果我们在 if 语句块外边,输入 test.to 那么 ide 给我们提供的智能提示中,会展示哪些 api 呢?

image.png

答案是:string 和 number 都可以访问的一些 api。

image.pngimage.png

void 类型

function printMenu() { // function printMenu(): void
  console.log("1. login");
  console.log("2. singn up");
}
/*
void 表示该函数没有返回值
*/

never 类型

function throwError(msg: string): never {
  throw new Error(msg);
  console.log("123"); // 永远不会执行
}
/*
若我们不指定 throwError 函数的返回值,那么它推断出来的结果是:
  function throwError(msg: string): void
在这种情况下,我们就需要手动约束,指定其返回值为 never,表示该函数永远不可能结束。
*/

image.png

function alwaysDoSomething(): never {
  while (true) {
    // ...
  }
}
/*
若不进行约束,那么识别出函数的返回值为 void
  function alwaysDoSomething(): void
此时,也需要我们进行手动约束。
*/

image.png

上述两种情况都会导致函数永远不会结束

  1. 函数执行过程中发生了错误
  2. 函数中出现了死循环

字面量类型

let gender: "male" | "female";
gender = "male";
/*
字面量约束,是一种很强力的约束,我们只能从指定的字面量中选值。
*/

image.png

let arr: [];

这也是一个字面量约束,表示 arr 只能赋值为一个空数组。

// 对象的字面量约束(注意语法)
let user: {
  name: string;
  age: number;
};

user = {
  name: "dahuyou",
  age: 22,
};
/*
对象也可以有字面量约束。
上述代码约束 user 对象需要具有两个属性:name,age;
并且:name 必须是 string 类型,age 必须是 number 类型;
*/

:::warning 我们一般不会使用上面这种写法来对一个对象进行约束,在学习了后边的知识后,我们一般会采用“接口”、“类”、“类型别名”。。。来对对象类型的数据进行约束。 :::

元祖类型

let tuple: [number, string];
tuple = [1, "1"];
/*
元祖类型(Tuple):一个固定长度的数组,并且数组中的每一项的类型确定;
要求 tuple 必须是一个数组,该数组必须有两项,第一项必须是 number 类型、第二项必须是 string 类型。
*/

any 类型

let data: any = "123";
let num: number = data;
/*
any 类型:any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型;
上述代码不会报错,但是,我们往往不希望有这样的行为,所以 any 类型,非必要的情况下,最好不要使用。
*/

3-4. 类型别名

前言

任务列表

  • 掌握类型别名的语法

:::tips 类型别名在实际开发中,偶尔会使用到,但是使用的评率并不高,因为后边还有更好地方式来取代它。 :::

notes

类型别名

类型别名用于对已知的一些类型定义名称,类型别名并不会生成到编译结果中。

语法:type 类型名称 = 具体的类型信息

codes

未使用类型别名

// 用户对象的类型约束
let u: {
  name: string;
  age: number;
  gender: "male" | "female";
};

// 获取用户对象
function getUsers(): {
  name: string;
  age: number;
  gender: "male" | "female";
}[] {
  return [];
}

获取用户对象的方法,返回的是一个数组,数组的每一项都是一个 u。

使用上面这种写法,当然也可以实现效果,但是,这么写很不好,存在的主要问题有:

  1. 可读性差;
  2. 不易维护;

若用户对象的类型约束(对象结构)发生了变化,那么需要改动的地方很多;
咋们写代码讲究:高内聚、低耦合。

:::warning 多个字段之间的分隔符:
name: string; ✅ 后面可以啥都不加
name: string ✅ 也可以加分号
name: string, ✅ 也可以加逗号 :::

let u;
function getUsers() {
  return [];
}

使用类型别名

type Gender = "male" | "female";
type User = {
  name: string;
  age: number;
  gender: Gender;
};

let u: User;

function getUsers(g: Gender): User[] {
  return [];
}

image.png

let u;
function getUsers(g) {
  return [];
}

Q & A

🤔 一句话介绍「类型别名」 类型别名就是将类型信息给记录到一个变量中,使用该变量就等价于使用该类型。

3-5. 函数的相关约束

前言

任务列表

  • 函数重载
  • 可选参数
  • 默认参数

notes

函数重载

在函数(定义)实现之前,对函数调用的多种情况进行声明。

不会生成到编辑结果中。

可选参数

可以在某些参数名后加上问号,表示该参数可以不用传递。

不会生成到编辑结果中。

:::warning 可选参数必须在参数列表的末尾。

翻译一下这句话:可选参数后边的所有参数,必须都得是可选的。 :::

默认参数

语法和含义和 es6 一致。

:::warning 默认参数必然是可选参数。 :::

codes

函数重载

function combine(a: number | string, b: number | string): number | string {
  if (typeof a === "number" && typeof b === "number") {
    return a * b;
  } else if (typeof a === "string" && typeof b === "string") {
    return a + b;
  }
}

这种写法未罗列出所有可能的情况,会报错。

image.png

function combine(a: number | string, b: number | string): number | string {
  if (typeof a === "number" && typeof b === "number") {
    return a * b;
  } else if (typeof a === "string" && typeof b === "string") {
    return a + b;
  }
  throw new Error("a 和 b 必须是相同的类型");
}

对于未能罗列出来的所有可能,我们可以通过 throw 一个错误来表示。

function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: number | string, b: number | string): number | string {
  if (typeof a === "number" && typeof b === "number") {
    return a * b;
  } else if (typeof a === "string" && typeof b === "string") {
    return a + b;
  }
  throw new Error("a 和 b 必须是相同的类型");
}

combine(1, 2); // => 2
combine("1", "2"); // => 12
// combine(1, '2');

当我们写了函数重载后,再去调用函数,就会有智能提示:

image.png

我们还可以给每个重载添加上注释,这样我们在调用函数的时候,我们写的相关注释内容,也会显示出来。

/**
 * 得到 a 和 b 相乘的结果
 * @param a
 * @param b
 */
function combine(a: number, b: number): number;
/**
 * 得到 a 和 b 拼接的结果
 * @param a
 * @param b
 */
function combine(a: string, b: string): string;
// ...

image.png

可以上下翻页,共两页,表示该函数的参数传递只能有两种情况。

可选参数

function sum(a: number, b: number, c?: number) {
  if (c) return a + b + c;
  return a + b;
}

sum(1, 2); // => 3
sum(1, 2, 3); // => 6

可选参数,表示该参数可以不传递。

在冒号前面加上一个问号,表示该参数是可选的 ?:

其中 c 就是可选参数,它可能是 undefined 或 number。

image.png

默认参数

function sum_3(a: number, b: number, c: number = 3) {
  if (c) return a + b + c;
  return a + b;
}

sum_3(1, 2); // => 6

默认参数,写法上和 es6 的默认参数写法相同。

默认参数表示当我们没有传递这个参数时,该参数的值是什么。

由此可知,默认参数必然是可选的。

image.png

类型推断的结果:function sum_3(a: number, b: number, c?: number): number

从类型推断的结果中我们发现,c 被识别为一个可选参数。

function test(a: number, b: number, c?: number) {}
function test(a: number, b?: number, c?: number) {}
function test(a?: number, b?: number, c?: number) {}

因为我们在调用函数的时候,参数都是挨个传递的,不可能说我们传递了第二个参数,而没有传递第一个参数。

由此我们不难得出以下结论:

  • 可选参数可以有多个,但是一定要出现在末尾。
  • 若一个参数是可选的,那么后续参数必然也是可选的。

Q & A

🤔 默认参数一定是可选参数? yes

🤔 如果某个函数有多个参数,那么可选参数是否可以写在前边? no

放在可选参数后边的参数,必然也是可选参数。

// ✅
function test(a: number, b: number, c?: number) {
  // ...
}

// ✅
function test(a: number, b?: number, c?: number) {
  // ...
}

// ✅
function test(a?: number, b?: number, c?: number) {
  // ...
}

// ❎
function test(a?: number, b: number, c: number) {
  // ...
}

3-6. 练习:创建并打印扑克牌

打印扑克,忽略大小王。

参考代码

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);

🙈 老师代码,自己实现一遍

开发前的准备工作:

使用tsc --init 命令来初始化一个 ts 的配置文件,并编写好基本配置信息。

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "lib": [
      "es2016"
    ],
    "outDir": "./dist",
    "strictNullChecks": true
  },
  "include": [
    "./src"
  ],
}

编辑 package.json 文件,简化执行流程。

{
  "scripts": {
    "dev": "nodemon --watch src -e ts --exec ts-node ./src/pocker.practice.ts"
  },
  "devDependencies": {
    "@types/node": "^16.11.26"
  }
}
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 = i;
    if (i === 1) number = "A";
    if (i === 11) number = "J";
    if (i === 12) number = "Q";
    if (i === 13) number = "K";
    deck.push({
      number,
      color: "❤",
    });
    deck.push({
      number,
      color: "♠",
    });
    deck.push({
      number,
      color: "♣",
    });
    deck.push({
      number,
      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);

export {};

写了一个类似于斗地主发牌的 demo。

image.png