参考资料
TS 官网 Object Types:链接

image.png

6-1. 接口的概念

前言

任务列表

  • 理解 TS 中接口的概念

理解笔记中所说的「弱标准」、「强标准」是啥意思。

notes

扩展类型:类型别名(type)、枚举(enum)、接口(interface)、类(class)

我们之前学习过 type、enum,本章节将要介绍的是 interface 接口。

首先,来了解一下接口的概念。

接口:interface,它是一种拓展类型 ,是用于约束类、对象、函数的契约(标准)。

契约(标准)的常见形式:

  1. API 文档(弱标准)
  2. 代码约束(强标准)

API 文档(弱标准):

平时我们接触到的 API 文档,其实就是一种契约、一种标准。前后端开发者都按照接口文档中的规定进行开发,遵循其中的规定。

但是这种契约仅仅是一种弱标准,没有强制的约束,开发者写出来的程序是否符合接口文档中的规定是不一定的。

:::tips 这里所说的「不一定」,主要是指,如果我们没有按照 API 文档的要求来写,代码并不会报错。强制要求我们必须按照标准来。

所以说这种契约(标准)是一种弱标准。 :::

代码约束(强标准):

代码约束其实就是使用 TS 提供的类型系统来约束。代码约束是强标准,我们写的代码都必须遵循这些标准,否则程序没法跑。

6-2. 接口的使用

前言

任务列表

  • 掌握接口的使用
  • 掌握接口的继承
  • 掌握类型别名中的交叉类型的语法
  • 掌握约束一个函数类型的多种写法
  • 理解定界符{}

PS:定界符这种叫法,上网搜了好久,依旧没有搜到。直接看笔记叭,袁老对定界符的介绍还是很容易理解的。

版本不同导致的差异: 对于类型别名使用交叉类型出现冲突的现象,袁进老师视频中展示的效果是两个类型合并,实际结果的是 never 类型。

  1. type A = {
  2. T1: string;
  3. };
  4. type B = {
  5. T1: number;
  6. T2: number;
  7. } & A;
  8. const b: B = {
  9. T1: "123",
  10. T2: 123,
  11. }

以该 demo 为例,对于 b.T1 的数据类型:

  1. 袁老版本:string & number
  2. 我的版本:never

notes

接口是用来约束类、对象、函数的契约。

interface


定义接口使用 interface 关键字

下面先介绍如何使用接口来约束「对象」和「函数」,暂不介绍「类」。

:::tips 有关「TS 的类」的相关知识,在后续课程中「7. TS中的类」会介绍到,当讲解类的相关知识点时,会介绍如何使用接口来约束类。 :::

约束「对象」

interface User {
  name: string;
  age: number;
  sayHello: () => void; // 等效写法 sayHello(): void
}

let u: User = {
  name: "dahuyou",
  age: 23,
  sayHello() {
    console.log("hello world");
  },
};
type User = {
  name: string;
  age: number;
  sayHello: () => void;
};

let u: User = {
  name: "dahuyou",
  age: 23,
  sayHello() {
    console.log("hello world");
  },
};
let u = {
  name: "dahuyou",
  age: 23,
  sayHello() {
    console.log("hello world");
  },
};
  • sayHello: () => viodsayHello(): void

这两种写法都是等效的,都是约束 sayHello 函数:

  1. 没有参数
  2. 没有返回值

约束「函数」

需求介绍:求和函数,针对满足特定条件的成员进行求和。

:::tips 这里说的满足特定条件,比如说:

  • 仅对数组的奇数项进行求和
  • 仅对数组的偶数项进行求和
  • 仅对数组的正数进行求和
  • 等等。。。

特定条件具体是啥,由用户指定。这种场景其实在我们写代码的时候非常常见,比如数组的 filter 方法,对数组进行过滤,过滤的条件是由我们自行指定的。

这时候,就得用到回调函数。 :::

需求实现:

  1. 定义一个 sum 函数
  2. sum 函数的第一个参数是一个数字数组
  3. sum 函数的第二个参数是一个回调函数,将回调返回为 true 的所有成员进行求和返回
function sum(numbers: number[], callBack: (n: number) => boolean) {
  let s = 0;
  numbers.forEach((n) => {
    if (callBack(n)) {
      s += n;
    }
  });
  return s;
}
  • (n: number) => boolean
    这一部分其实可以单独提取一个类型别名或接口出来
// 写法1
interface Condition {
    (n: number): boolean
}

// 写法2
// type Condition = (n: number) => boolean

// 写法3
// type Condition = {
//     (n: number): boolean
// }

function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach((n) => {
    if (callBack(n)) {
      s += n;
    }
  });
  console.log(s);
  return s;
}
sum([1, 2, -3, 4], n => n > 0); // => 7
sum([1, 2, -3, 4], function(n) {
    return n > 0 && n % 2 === 0;
}); // => 6

定界符 {}

interface Condition { // 定界符
    (n: number): boolean
}
type Condition = { // 定界符
    (n: number): boolean
}

定界符的概念:
在定义一个接口、类型别名时,使用到了 {}。如果里面没有一个属性名,放的是一个具体的约束内容,那么 {} 表示定界符,并不表示当前约束的是一个对象。

:::tips Q:定界符是啥?

就是以上定义「函数接口」语法中的 {} 符号。

上网搜过,没找到答案,袁老在视频中也没详细介绍,估计这东西不重要。 :::

继承和交叉类型

  • 接口,使用 extends

使用 extends 关键字,可以实现接口的继承,通过接口之间的继承,实现多种接口的组合

  • 类型别名,使用 &

使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型

interface A {
  T1: string;
}

interface B extends A {
  T2: number;
}

interface C extends A, B {
  T3: boolean;
}

let b: B = {
  T1: "123",
  T2: 123,
};

let c: C = {
  T1: "1",
  T2: 1,
  T3: false,
};
type A = {
  T1: string;
};

type B = {
  T2: number;
} & A;

type C = {
  T3: boolean;
} & A &
  B;

let b: B = {
  T1: "123",
  T2: 123,
};

let c: C = {
  T1: "1",
  T2: 1,
  T3: false,
};

区别 - 对冲突项的处理:

  1. 子接口不能覆盖父接口的成员
  2. 交叉类型会把相同成员的类型进行交叉
interface A {
  T1: string;
}

interface B extends A {
  T1: number,
  T2: number;
}

image.png
从提示信息告诉我们:属性 T1 的类型不能共存(incompatible),类型 number 不能分配(assignable)给类型 string

这种报错是合情合理的,也是我们希望看到的,可以让我们尽早地发现问题,修改错误。

type A = {
  T1: string;
};

type B = {
  T1: number;
  T2: number;
} & A;

const b: B = {
  T1: "123", // 123
  T2: 123,
}

要求 T1 既是一个 string 类型,又是一个 number 类型。

没有值能够赋值给 T1,它将被识别为 never 类型。

image.png

要求一个成员,即是 number 又是 string,该成员同时拥有 numberstring 原型上的成员。没有任何一个值可以满足要求,即是 number 又是 string,所以 T1 的结果是 never 类型。

报错

虽然也能提供报错信息,但是提供报错的时间点不同。类型别名 B 在定义的时候,并没有报错。当我们在使用类型别名 B 来约束变量的类型时,才会意识到出现问题了。

Q & A

🤔 接口和类型别名之间的区别? 就目前阶段来看,接口和类型别名非常类似,除了写法上的一些细微差异外,似乎没啥区别。

type 和 interface 之间的区别主要体现在约束「类」。

就编译结果而言,接口和类型别名都不会出现在编译结果中。

这个问题在本节所介绍的内容中,会简单提及一些,若要了解更多,还得学习完下一章「TS 的类」之后才清楚。

PS:进阶阶段,才会介绍「接口」和「类」如何连用,才会知道「接口」和「类型别名」之间真正的区别,现阶段就简单地认为它俩是一致的即可。

🤔 如果某些场景,即可以使用接口,也可以使用类型别名,那么我们应该使用哪个? 袁进老师推荐使用「接口」

虽然有些教程说,根据习惯来,喜欢哪个用哪个。

不过蛮多主流的第三方库,它们采用的大多都是 interface。这个就看个人习惯叭,个人感觉既然都行,正好现在还没养成习惯,那就按袁老的建议来。

6-3. readonly 修饰符

前言

任务列表

  • 掌握 readonly 修饰符的用法

notes

本节介绍 readonly 修饰符,它是本教程中介绍的第一个修饰符,在之后介绍「类」时,还会介绍其它修饰符。

readonly 是一个修饰符,表示只读,被 readonly 修饰符修饰的目标是无法修改的。readonly 修饰符不会出现在编译结果中。

重点关注在使用它修饰对象的属性时的作用位置。这里先抛出一个问题:

interface A {
  T1: readonly number[]
}

interface A {
  readonly T1: readonly number[]
}

codes

修饰属性

interface User {
  id: string
  readonly name: string
  age: number
}

let u: User = {
  id: "123",
  name: "dahuyou",
  age: 23
}

u.id = "abc"; // 允许修改
u.name = "xiaohuyou"; // 报错

image.png
错误提示:我们无法给只读的属性 name 重新赋值。

修饰变量

let arr: readonly number[] = [1, 2, 3];
arr = [4, 5, 6]; // √
arr[0] = 10; // ×
arr.push(10); // ×

被允许的操作:
arr = [4, 5, 6]
若我们重新给数组赋值,这是允许的。

:::tips 决定是否能给变量 arr 重新赋值的是:是否使用了 const 关键字来声明变量。

这压根就不需要借助 ts 来帮我们搞定,使用 js 就完全够了。 :::

不被允许的操作:
arr[0] = 4arr.push(10)
若我们想要修改数组中某个成员的值,或者改变数组内部的成员,这是不被允许的。

对于被 readonly 修饰符修饰的数组,它们身上的那些但凡是可以改变原数组的 api 均没法访问。比如:poppushsplice 等等。

image.png
image.png
小结:
这里所说的只读数组,是指数组的内部不可变,并不意味着不能重新给变量 arr 赋值。

等效写法:

  1. let arr: readonly number[]
  2. let arr: ReadonlyArray<number>

对比 readonly 修饰符作用于不同的位置

interface A {
  T1: readonly number[]
}

const a: A = {
  T1: [1, 2, 3]
}

a.T1 = ['a', 'b', 'c']; // √
a.T1[0] = 'a'; // ×
interface A {
  readonly T1: readonly number[]
}

const a: A = {
  T1: [1, 2, 3]
}

a.T1 = ['a', 'b', 'c']; // ×
a.T1[0] = 'a'; // ×

6-4. 类型兼容性

前言

任务列表

  • 理解类型兼容性的概念
  • 鸭子辨型法
  • 理解基本类型的类型兼容性判断逻辑 - 严格按照类型约束来
  • 理解对象类型的两种类型兼容性判断逻辑
    • 变量赋值 - 鸭子辨型法
    • 字面量赋值 - 严格按照约束来
  • 理解函数类型的类型兼容性判断逻辑
    • 参数:可少不可多
    • 返回值:有要求则严格按照要求来,若没要求(也就是 void 类型)则随意
  • 类型断言
    • xxx as xxx
    • <xxx>xxx

notes

类型兼容性

类型兼容性,就是指 TS 中是如何判断不同类型之间的数据是否能够完成赋值。

  • 若类型检查能够通过,那么意味着可以赋值
  • 若类型检查无法通过,那么意味着不能赋值

将变量 B 赋值给变量 A,如果能够完成赋值,则称 B 和 A 类型兼容。

鸭子辨型法

鸭子辨型法,也称为子结构辨型法。

目标类型需要具备某一些特征,赋值的类型只要能满足(包含)这些特征即可。

基本类型

要求完全匹配

对象类型

赋的值来自于一个:

  • 变量:鸭子辨型法
  • 字面量:必须要严格满足类型要求

函数类型

  • 参数:传递给目标函数的参数可以少,但不可以多
  • 返回值:
    • 若有要求返回值,那么必须按照约束条件返回指定类型的数据
    • 若没有要求返回值(也就是 void),那么我们可以随意处理

类型断言

xxx as xxx

  • 前:值
  • 后:类型

类型断言的其它写法:<xxx>xxx

  • 前:类型
  • 后:值

codes

对象类型

变量赋值

interface Duck {
  sound: "嘎嘎嘎" // "嘎嘎嘎" 表示字面量类型
  swin(): void
}

let person = {
  name: "伪装成鸭子的人",
  age: 11,
  sound: "嘎嘎嘎" as "嘎嘎嘎", // 第一个 "嘎嘎嘎" 表示值,第二个 "嘎嘎嘎" 表示字面量类型
  // sound: <"嘎嘎嘎">"嘎嘎嘎"
  swin() {
    console.log(`${this.name}正在游泳,并发出了${this.sound}的声音。`);
  }
}

let duck: Duck = person; // √

let duck: Duck = person 这条语句是能够通过类型检查的,因为我们约束变量 duck 是一个 Duck 类型,并且 person 中包含 Duck 类型所需要的所有成员,类型也都满足要求。

image.png
由于我们使用接口 Duck 限制的 duck 的类型,所以此时我们仅能访问 soundswin

如果我们尝试访问 nameage,那么会提示 Duck 类型中不存在这些属性。

image.png

虽然我们在目标文件 .ts 中无法访问这些属性,但是并不意味着它们在编译结果中不存在。

在编译结果中 duck 还是含有 4 个属性的

let person = {
  name: "伪装成鸭子的人",
  age: 11,
  sound: "嘎嘎嘎",
  swin() {
    console.log(`${this.name}正在游泳,并发出了${this.sound}的声音。`);
  }
};
let duck = person;

TS 这么处理是非常合理的

假设现在有这么一个场景:有一个用于获取用户信息的 api,但是这个 api 请求到的用户信息中有大量字段,而我们目前正在写的需求需要调用这个 api 获取用户信息数据,我们仅需要其中的某几个字段的值即可。

很多情况下,我们所需要的数据都是调用某个 api 才获取到的,对于后端返回给我们的数据,我们只需要用到其中的一小部分即可。

我们知道的是:我们需要哪些字段;
我们不想关心的是:api 拿到的数据的详细数据结构信息;

那么接下来我们可以这么做:

  1. 依据我们所需要的字段来定义接口
  2. 调用 api 获取到相关数据 data
  3. 定义一个变量 a,要求变量 a 实现第一步定义的接口
  4. 使用 data 给 a 进行赋值
(async() => {
  interface Idemo {
    // ...(罗列出我们需要的字段)
  }
  const data = await getDatas();
  const a: Idemo = data;
})();

通过上述步骤,就能帮助我们在开发阶段(书写 TS 代码)时,过滤掉那些我们不需要的字段。

字面量赋值

interface Duck {
  sound: "嘎嘎嘎" // "嘎嘎嘎" 表示字面量类型
  swin(): void
}

let duck: Duck = {
  name: "伪装成鸭子的人",
  age: 11,
  sound: "嘎嘎嘎" as "嘎嘎嘎", // 第一个 "嘎嘎嘎" 表示值,第二个 "嘎嘎嘎" 表示字面量类型
  swin() {
    console.log(`${this.name}正在游泳,并发出了${this.sound}的声音。`);
  }
}; // ×

这段程序和上边的程序看似相同,但是是不被允许的,类型检查无法通过。

如果我们在定义一个变量时,通过字面量的形式来对其进行赋值。说明我们显然知道接下来都要写哪些属性,这时候再刻意的去写接口 Duck 中不存在的属性,显然是不合规矩的。

image.png

对比:字面量赋值、变量赋值

interface User {
  name?: string
  age: number
}

let u: User = {
  nema: "dahuyou", // ×
  age: 23
}

User 接口约束:(通过**字面量**形式赋值)

  1. 必须要有 age 属性,属性值必须是 number 类型
  2. 可以有 name 属性,也可以没有 name 属性,若有 name 属性,属性值必须是 string 类型
  3. 不能有其它属性

image.png

通过字面量的形式来赋值,类型检查很严格,必须和接口约束完全相同才行。

interface User {
  name?: string
  age: number
}

let user = {
  nema: "dahuyou",
  age: 23
};

let u: User = user; // √

此时采用的是鸭子辨型法来判断。

这么写是不会报错的,因为 user 中含有必须的数值属性 age,可选属性 name 是可有可无的。

User 接口约束:(通过**变量**形式赋值)

  1. 必须要有 age 属性,属性值必须是 number 类型
  2. 可以有 name 属性,也可以没有 name 属性,若有 name 属性,属性值必须是 string 类型
  3. 不能有其它属性


函数类型

interface Condition {
  (n: number, i: number): boolean;
}

function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach((n, i) => {
    if (callBack(n, i)) s += n;
  });
  return s;
}

console.log(sum([1, 2, 3, 4], (n) => n % 2 === 0));

image.png

  • 参数可少不可多;
  • 如果有要求返回值类型,那么返回值类型必须满足要求;

如果在约束时,要求的返回值类型是 void 类型,那么返回值可随意

6-5. 练习:用接口改造扑克牌程序

基于上一章最后一节的练习继续优化

任务列表

  • 独立完成本节练习
import {createDeck, printDeck} from "./funcs"

const deck = createDeck();

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

// one,two,three,four,five,six,seven,eight,nine,ten,eleven,twelve,thirteen
// 牌编号
export 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",
}
import { Colors, Nums } from "./enums";

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

export type Deck = Poker[]; // 一副牌
import { Colors, Nums } from "./enums";

export interface Poker { // 一张牌
  getString: () => void
}

export interface JokerPoker extends Poker { // 一张大、小王
  type: "big" | "small"
}

export interface NormalPoker extends Poker { // 一张普通的牌
  number: Nums;
  color: Colors;
};

export type Deck = Poker[]; // 一副牌
import { Colors, Nums } from "./enums";

export interface JokerPoker { // 一张大、小王
  type: "big" | "small"
  getString: () => void
}

export interface NormalPoker { // 一张普通的牌
  number: Nums;
  color: Colors;
  getString: () => void
};

export type Deck = (JokerPoker | NormalPoker)[];
import { Colors, Nums } from "./enums";
import { Deck } from "./types";

// 创建一副牌
export 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;
}

// 打印一副扑克
export 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);
}
import { Colors, Nums } from "./enums";
import { Deck, JokerPoker, NormalPoker } from "./types";

// 创建一副牌
export function createDeck() {
  const deck: Deck = [];
  // 插入大小王
  let jo: JokerPoker = {
      type: "small",
      getString() {
        return "joker";
      },
    },
    JO: JokerPoker = {
      type: "big",
      getString() {
        return "JOKER";
      },
    };
  deck.push(jo, JO);
  // 插入 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++) {
      const normalPoker: NormalPoker = {
        number: Nums[numKeys[j]],
        color: Colors[colorKeys[i]],
        getString() {
          return `${this.color}${this.number}`;
        },
      };
      deck.push(normalPoker);
    }
  }
  deck.sort(() => Math.random() - 0.5); // 打乱牌序
  return deck;
}

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

image.png

// 写法1
deck.push({
  number: Nums[numKeys[j]],
  color: Colors[colorKeys[i]],
  getString() {
    return `${this.color}${this.number}`;
  },
} as NormalPoker);
// 写法2
deck.push(<NormalPoker>{
  number: Nums[numKeys[j]],
  color: Colors[colorKeys[i]],
  getString() {
    return `${this.color}${this.number}`;
  },
});

image.png