五、TypeScript基础

郁子大约 35 分钟约 10462 字笔记渡一教育强化课袁进

(一)概述

官方网站open in new window


个人翻译中文网open in new window

1.为什么要学习 TypeScript

  • 就业
  • 获得更大的竞争优势
  • 获得更好的开发体验
  • 解决 JS 中一些难以处理的问题

2.JS 开发中的问题

  • 使用了不存在的变量、函数或成员
  • 把一个不确定的类型当作一个确定的类型处理
  • 使用 null 或 undefined 的成员
// 有错误的代码
function getUserName() {
  if (Math.random() < 0.5) {
    return "yuan jin";
  }
  return 404;
}

let myname = getUsername();
mynema = myname
  .split(" ")
  .filter((it) => it)
  .map((it) => it[0].toupperCase() + it.substr(1))
  .join(" ");








 
 


 

3.JS 的原罪

  • JS 语言本身的特性
    • 决定了该语言无法适应大型的复杂的项目
  • 弱类型语言
    • 某个变量可以随时更换类型
  • 解释性语言
    • 错误发生在运行时

4.TypeScript 简介

  • 简称 TS,是 JS 的超集
  • 是一个可选的、静态的类型系统

1)超集

  • 类比整数和正整数
  • 整数是正整数的超集

2)类型系统

  • 对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查
  • 要构建大型的应用,会涉及大量的函数和接口
    • 如果没有类型检查,会产生大量的调试成本
    • 类型系统可以降低调试成本,从而降低开发成本
function getUserName(): string | number {
  if (Math.random() < 0.5) {
    return "yuan jin";
  }
  return 404;
}
let myName = getUserName();
if (typeof myName === "string") {
  myName = myName
    .split(" ")
    .filter((it) => it)
    .map((it) => it[0].toUpperCase() + it.substring(1))
    .join(" ");
}

3)可选的

  • 学习曲线非常平滑
  • JS 的所有功能都能够在 TS 中使用
    • 增加的部分是类型系统

4)静态的

  • 在运行之前 是静态的
  • 无论是浏览器环境,还是 Node 环境,都无法直接识别 ts 代码
    • babel: es6 -> es5
    • tsc: ts -> es
  • tsc
    • TS 编译器
    • TS 代码 -> 编译 -> JS 代码
  • 静态
    • 类型检查发生的时间,是在 编译时 而非运行时
    • TS 不参与任何运行时的类型检查
      • 运行的是编译后的 JS 代码

5)TS 的常识

  • 2012 年微软发布(ES6/ES2015)
  • Anders Hejlsberg 负责开发 TS 项目
  • 开源、拥抱 ES 标准

6)额外的惊喜

  • 有了类型检查,增强了面向对象的开发
    • JS 中也有类和对象,支持面向对象开发
    • 但是没有类型检查,很多面向对象的场景实现起来有诸多问题
  • 使用 TS 后,可以编写出完善的面向对象代码

(二)在 Node 中搭建 TS 开发环境

1.安装 TypeScript

npm i -g typescript

1)默认编译选项

  • 假设当前的执行环境是 DOM(浏览器环境)
  • 如果代码中没有使用模块化语句(import、export),则认为该代码是全局执行
  • 编译的目标代码是 ES3,发挥最大的兼容性

2)更改编译选项

  • 方式一:使用 tsc 命令行时加上选项参数
tsc index.ts
  • 方式二:使用 ts 配置文件更改编译选项
tsc --init

2.TS 的配置文件

// tsconfig.json
{
  "compilerOptions": {
    // 编译选项
    "target": "es2016", // 配置编译目标代码的版本标准
    "module": "commonjs", // 配置编译目标使用的模块化标准
    "lib": ["es2016"],
    "outDir": "./dist"
  },
  "include": ["./src"]
}
  • 如果使用配置文件,则在使用 tsc 进行编译时不能跟上文件名
tsc
let abc = "asdf";
  • 如果跟上文件名,会忽略配置文件
tsc ./src/index.ts
var abc = "asdf";

3.@types/node

  • @types 是一个 TS 官方的类型库
  • 包含了很多对 JS 代码的类型描述
  • 如:JQuery 库是用 JS 写的,没有类型检查
    • 安装 @types/jquery 可以为 JQuery 库添加类型定义
npm i -g -D @types/node

4.使用第三方库简化流程

// package.json
{
  "scripts": {
    "dev": "nodemon --watch src -e ts --exec ts-node src/index.ts"
  },
  "devDependencies": {
    "@types/node": "^11.13.7"
  }
}

1)ts-node

  • 将 TS 代码在内存中完成编译,同时完成运行
  • 不生成编译后的 JS 文件
npm i -g ts-node
ts-node src/index.ts

2)nodemon

  • 检测文件的变化
npm i -g nodemon
ts-node src/index.ts

3)流程

  • 开发过程中使用 npm run dev 编译运行 TS 文件
  • 开发完成后使用 tsc 打包输出 dist 目录下的 JS 文件

(三)基本类型检查

1.基本类型约束

1)类型约束

  • 仅需要在变量、函数的参数、函数的返回值位置加上 :类型
let name: string;
name = "333";

function sum(a: number, b: number): number {
  return a + b;
}
let num: number = sum(3, 4);
  • TS 在很多场景中可以完成类型推导
// 智能推导出返回值为 number 类型
function sum(a: number, b: number) {
  return a + b;
}
let num = sum(3, 4);
  • 如果推导不出类型,自动设置为 any 类型
  • 表示任意类型,TS 不对该类型进行类型检查
let age;

如何区分数字字符串和数字

  • 关键看怎么读
  • 如果按照数字的方式朗读,则为数字
    • 如:¥100.00 读作 一百元,是数字
    • let price: number = 100.00
  • 否则,为字符串
    • 如:138****8085 读作 一三八****八零八五,是字符串
    • let phone: string = "138****8085"

2)源代码和编译结果的差异

  • 编译结果中没有类型约束信息
let name;
name = "333";

function sum(a, b) {
  return a + b;
}
let num = sum(3, 4);

2.基本类型

类型含义
number数字
string字符串
boolean布尔
array数组
必须指定每一项的类型
object对象
约束力不强,无法约束每一个属性的类型
null 和 undefined是其他类型的子类型
可以赋值给其他类型
function isOdd(n: number) {
  return n % 2 === 0;
}

let nums: number[] = [3, 4, 5]; // 语法糖
let nums2: Array<number> = [3, 4, 5];

function printValues(obj: object) {
  const values = Object.values(obj);
  values.forEach((v) => console.log(v));
}
printValues({
  name: "afd",
  age: 33,
});

let n: string = undefined;
n.toUpperCase();
  • 配置 strictNullChecks: true 可以获得更严格的空类型检查
  • 此时 null 和 undefined 只能赋值给自身
{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

3.其他常用类型

1)联合类型

  • 多种类型任选其一
  • 配合类型保护进行判断
  • 类型保护
    • 当对某个变量进行类型判断后,在判断的语句块中便可以确定它的确切类型
    • typeof 可以触发 简单类型 的类型保护
let name1: string | undefined;
if (typeof name1 === "string") {
  // 类型保护
  console.log(name.length);
}

2)void 类型

  • 通常用于约束函数的返回值,表示该函数没有任何返回
function printMenu() {
  console.log("1. 登录");
  console.log("2. 注册");
}

3)never 类型

  • 通常用于约束函数的返回值,表示该函数永远不可能结束
function throwError(msg: string): never {
  throw new Error(msg);
}
function alwaysDoSomething(): never {
  while (true) {
    //...
  }
}

4)字面量类型

  • 使用一个值进行约束
let a: "A";
a = "A";
// a = 'B'; // 报错

let gender: "男" | "女";
gender = "女";
gender = "男";

let arr: []; // arr永远只能取值为一个空数组
  • 对象字面量可以更加细化地约束一个对象
  • 通常使用接口或类约束对象
let user: {
  name: string;
  age: number;
};
user = {
  name: "34",
  age: 33,
};

5)元祖类型(Tuple)

  • 一个固定长度的数组,并且数组中每一项的类型确定
let tu: [string, number];
tu = ["3", 4];

6)any 类型

  • any 类型可以绕过类型检查
  • 因此,any 类型的数据可以赋值给任意类型
  • 不要随意使用 any
let data: any = "sfdsdf";
let num3: number = data;

4.类型别名

  • 对已知的一些类型定义名称
  • type 类型名 = ...
type Gender = "男" | "女";
type User = {
  name: string;
  age: number;
  gender: Gender;
};

let u: User;
u = {
  name: "sdfd",
  gender: "男",
  age: 34,
};

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

5.函数的相关约束

1)函数重载

  • 在函数实现之前,对函数调用的多种情况进行声明
// 声明不同类型参数的返回值类型,确保result类型【函数重载】
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必须是相同的类型");
}

const result1 = combine("a", "b");
const result2 = combine(1, 2);
console.log(result1, result2);

2)可选参数

  • 可以在某些参数名后加上问号,表示该参数可以不用传递
  • 可选参数必须在参数列表的末尾
  • 默认参数一定是可选参数
// function sum3(a: number, b: number, c: number = 0) {
function sum3(a: number, b: number, c?: number) {
  if (c) {
    return a + b + c;
  } else {
    return a + b;
  }
}
sum3(3, 4);
sum3(3, 4, 5);

6.案例实操:创建并打印扑克牌

  • 创建一副扑克牌(不包括大小王),打印该扑克牌
// 扑克牌花色的类型
type Color = "♥" | "♠" | "♦" | "♣";

// 一张扑克牌的类型
type NormalCard = {
  color: Color;
  mark: number;
};

// 一副扑克牌的类型
type Deck = NormalCard[];

/**
 * 创建一副扑克牌
 */
const createDeck = (): Deck => {
  const deck: Deck = [];
  for (let i = 1; i <= 13; i++) {
    deck.push({
      mark: i,
      color: "♠",
    });
    deck.push({
      mark: i,
      color: "♣",
    });
    deck.push({
      mark: i,
      color: "♥",
    });
    deck.push({
      mark: i,
      color: "♦",
    });
  }
  return deck;
};

/**
 * 打印扑克牌
 */
const printDeck = (deck: Deck) => {
  let result = "\n";
  deck.forEach((card, i) => {
    let str = card.color;
    if (card.mark <= 10) {
      str += card.mark;
    } else if (card.mark === 11) {
      str += "J";
    } else if (card.mark === 12) {
      str += "Q";
    } else {
      str += "K";
    }
    result += str + "\t";
    if ((i + 1) % 6 === 0) {
      result += "\n";
    }
  });
  console.log(result);
};

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

(四)扩展类型 —— 枚举

  • 枚举通常用于约束某个变量、参数、函数返回值的取值范围

1.扩展类型

  • 包括类型别名、枚举、接口、类
  • 类型别名、接口,不产生编译结果
  • 枚举、类,产生编译结果
    • 枚举 -> 编译 -> 对象
    • TS 类 -> 编译 -> JS 类

2.字面量类型的问题

  • 字面量和联合类型配合使用,也可以约束取值范围
let gender: "男" | "女";

1)在类型约束位置,会产生重复代码

  • 可以使用类型别名解决该问题
let gender: "男" | "女";
function searchUsers(g: "男" | "女") {}

2)逻辑含义和真实的值产生混淆

  • 会导致当修改真实值的时候,产生大量的修改
// type Gender = "男" | "女";
type Gender = "male" | "female";

// 使用真实值时都要一起修改
let gender: Gender;
gender = "男";
gender = "女";

3)字面量类型不会进入到编译结果

  • 无法动态读取字面量类型信息再显示到页面上

3.定义枚举

/*
enum 枚举名{
  枚举字段1 = 值1,
  枚举字段2 = 值2,
  ...
}
*/
enum Gender {
  male = "男",
  female = "女",
}

// 使用逻辑含义赋值
let gender: Gender;
gender = Gender.male;
gender = Gender.female;
  • 枚举会出现在编译结果中
  • 编译结果中表现为对象
var Gender;
(function (Gender) {
  Gender["male"] = "\u7537";
  Gender["female"] = "\u5973";
})(Gender || (Gender = {}));

// 使用逻辑含义赋值
let gender;
gender = Gender.male;
gender = Gender.female;

4.枚举的规则

  • 枚举的字段值可以是字符串或数字
  • 数字枚举的值会自动自增
  • 被数字枚举约束的变量,可以直接赋值为数字
enum Level {
  level1, // 0
  level2, // 1
  level3, // 2
}

let l: Level = Level.level1;
l = Level.level2;
// l = 2; // 不报错,不推荐
  • 数字枚举的编译结果和字符串枚举有差异
  • 如果要遍历输出需要考虑相关结果
var Level;
(function (Level) {
  Level[(Level["level1"] = 0)] = "level1";
  Level[(Level["level2"] = 1)] = "level2";
  Level[(Level["level3"] = 2)] = "level3";
})(Level || (Level = {}));

let l = Level.level1;
l = Level.level2;
/*
{
  level1: 0,
  level2: 1,
  level3: 2,
  0: "level1",
  1: "level2",
  2: "level3"
}
*/

最佳实践

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值

5.案例实操:使用枚举优化扑克牌

  • 使用枚举改造程序
// 扑克牌花色的类型
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",
  eleven = "J",
  twelve = "Q",
  king = "K",
}

// 一张扑克牌的类型
type NormalCard = {
  color: Color;
  mark: Mark;
};

// 一副扑克牌的类型
type Deck = NormalCard[];

/**
 * 创建一副扑克牌
 */
const createDeck = (): Deck => {
  const deck: Deck = [];
  const marks = Object.values(Mark);
  const colors = Object.values(Color);
  for (const m of marks) {
    for (const c of colors) {
      deck.push({
        color: c,
        mark: m,
      });
    }
  }
  return deck;
};

/**
 * 打印扑克牌
 */
const printDeck = (deck: Deck) => {
  let result = "\n";
  deck.forEach((card, i) => {
    let str = card.color + card.mark;
    result += str + "\t";
    if ((i + 1) % 6 === 0) {
      result += "\n";
    }
  });
  console.log(result);
};

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

6.位枚举【扩展】

  • 枚举的位运算
  • 针对数字枚举

1)位运算

  • 两个数字换算成二进制后进行的运算

2)枚举的组合

  • 需求:将多个枚举组合成新的枚举值
// // 无法快速穷举
// enum Permission {
//   Read,
//   Write,
//   Create,
//   Delete,
//   ReadAndWrite,
//   ReadAndWriteAndCreate,
//   // ...
// }

// 哪一位是1,表示有该位对应的权限
enum Permission {
  Read = 1, // 0001 2^0
  Write = 2, // 0010 2^1
  Create = 4, // 0100 2^2
  Delete = 8, // 1000 2^3
}
  • 使用或运算组合权限
  • 有 1 为 1
/**
 * 0001
 * 或
 * 0010
 * =>
 * 0011
 */
let p: Permission = Permission.Read | Permission.Write;
p = p | Permission.Delete;

2)判断是否拥有某种权限

  • 使用与运算判断权限
  • 全 1 为 1
/**
 * 0011
 * 且
 * 0010
 * =>
 * 0010
 */
function hasPermission(target: Permission, per: Permission) {
  return (target & per) === per;
}
//判断变量p是否拥有可读权限
console.log(hasPermission(p, Permission.Read));

3)删除某种权限

  • 使用异或运算删除权限
  • 不同为 1
/**
 * 0011
 * 异或
 * 0010
 * =>
 * 0001
 */
p = p ^ Permission.Write;
console.log(hasPermission(p, Permission.Write));

(五)TS 中的模块化

前端领域中的模块化标准

ES6CommonJS、AMD、UMD、System、ESNext

  • 在 TS 中导入和导出模块,统一使用 ES6 的模块化标准

1.ES 模块化代码

1)普通导出【推荐】

  • 导入的文件直接使用导出变量,会自动导入
  • 因为 TS 不允许为定义变量存在,会自动从模块中寻找
// 导出
export const name = "kevin";
export function sum(a: number, b: number) {
  return a + b;
}

// 导入
import { name, sum } from "./myModule";

2)默认导出

  • 导入的文件直接使用变量,不会自动导入
  • 因为默认导出是 default 对象,没有固定的名称
  • 具体使用的模块名称需要在导入时指定,所以没有智能提示
// 导出
export default {
  name: "kevin",
  sum(a: number, b: number) {
    return a + b;
  },
};

// 导入
import myModule from "./myModule";

2.编译结果中的模块化

# 编译并监听文件变化,不执行文件
tsc --watch

1)模块化可配置

配置名称含义
module设置编译结果中使用的模块化标准
moduleResolution设置解析模块的模式
noImplicitUseStrict编译结果中不包含 "use strict"
removeComments编译结果移除注释
noEmitOnError错误时不生成编译结果
esModuleInterop启用 ES 模块化非 ES 模块导出 交互
{
  "compilerOptions": {
    // 编译选项
    "target": "es2016", // 配置编译目标代码的版本标准
    // "module": "es6",
    "module": "commonjs", // 配置编译目标使用的模块化标准
    "lib": ["es2016"],
    "outDir": "./src/dist",
    "strictNullChecks": true,
    "removeComments": true,
    "noImplicitUseStrict": true,
    "esModuleInterop": true,
    "noEmitOnError": true,
    "moduleResolution": "node"
  },
  "include": ["./src"]
}

2)编译结果的模块化标准是 ES6

  • 编译结果和书写的代码没有区别

3)编译结果的模块化标准是 CommonJS

  • 导出的声明会变成 exports 的属性
  • 默认导出会变成 exports 的 default 属性
export const name = "kevin";
export function sum(a: number, b: number) {
  return a + b;
}

export default function () {
  console.log("hello my module!");
}
"use strict";

// 相当于 exports.__esModule = true;
Object.defineProperty(exports, "__esModule", { value: true });

exports.sum = exports.name = void 0;
exports.name = "kevin";
function sum(a, b) {
  return a + b;
}
exports.sum = sum;

function default_1() {
  console.log("hello my module!");
}
exports.default = default_1;

/*
相当于
module.exports = {
  name: "kevin",
  sum(a, b) {
    return a + b;
  },
};
*/
  • 导入的编译结果
import sayHello, { name, sum } from "./myModule";
console.log(name);
sayHello();
Object.defineProperty(exports, "__esModule", { value: true });

const myModule_1 = require("./myModule");

console.log(myModule_1.name);
(0, myModule_1.default)();

4)导入的模块不是 CommonJS 的导出格式

  • 如果导入了不是用 commonjs 导出的模块
  • 编译结果会调用 default 属性,导致报错
  • 配置 "esModuleInterop": true 也可以解决
// fs模块的导出方式是 module.exports = {}
import fs from "fs";
fs.readFileSync("./");
const fs_1 = require("fs");
fs_1.default.readFileSync("./");
  • 解决
import { readFileSync } from "fs";
readFileSync("./");

// 或者
import * as fs from "fs";
fs.readFileSync("./");
const fs_1 = require("fs");
(0, fs_1.readFileSync)("./");

// 或者
const fs = require("fs");
fs.readFileSync("./");

3.CommonJS 模块化代码

1)可以直接使用原有方式书写

  • 无法获得类型检查
// 导出
module.exports = {
  name: "kevin",
  sum(a: number, b: number) {
    return a + b;
  },
};

// 导入
const myModuleCommonJS = require("./myModule");
console.log(myModuleCommonJS);

2)使用 TS 要求的格式书写

// 导出
export = {
  name: "kevin",
  sum(a: number, b: number) {
    return a + b;
  },
};

// 导入
import myModule = require("./myModule");
  • 编译结果
"use strict";
module.exports = {
  name: "kevin",
  sum(a, b) {
    return a + b;
  },
};

4.模块解析

  • 应该从什么位置寻找模块
  • TS 中,有两种模块解析策略
    • classic:经典解析策略
    • node:Node 解析策略
      • 和 NodeJS 中一致
      • 唯一变化是将 js 替换为 ts

1)使用相对路径导入

  • 先找当前目录下是否有同名文件
  • 再找当前项目 package.json 是否配置了 main 字段
  • 再找同名目录下的 index.ts 文件
  • .ts => .json => .node => .mjs
// 相对路径
require("./xxx");

2)导入非相对模块

  • 先找当前项目的 node_modules 目录下是否有同名模块
  • 再往上级的 node_modules 目录寻找
// 非相对模块
require("xxx");

强制开启 node 解析策略

"moduleResolution": "node"

5.案例实操:使用模块化优化扑克牌

1)使用模块化

import { createDeck, printDeck } from "./utils";
import { Color, Mark } from "./enums";
import { Deck } from "./types";

2)编译结果

  • types 文件仅导出需要的类型
  • 编译后的 js 不需要这些类型
  • 所以编译结果为空,并且使用该类型的文件没有任何导入
// types.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

3)重置编译文件输出目录

  • 每次重新编译打包后,先删除 dist 目录,再输出打包文件
  • 配置命令
    • rd:remove directory
    • /s:删除文件夹及文件夹内的内容
    • /q:后续所有是否问题全部确认 y
{
  "scripts": {
    "build": "rd /s /q dist & tsc"
  }
}

(六)接口和类型兼容性

1.扩展类型 —— 接口 interface

1)TypeScript 的接口

  • 用于约束类、对象、函数的契约(标准)
  • 和 type 类型别名一样,接口 不出现在编译结果中
  • 和 type 定义的约束没有区别
    • 只在 约束类 的时候两者才有区别
  • 推荐使用 interface 约束

2)契约(标准)的形式

  • API 文档,弱标准
  • 代码约束,强标准

2.接口的使用

1)使用接口约束对象

interface User {
  name: string;
  age: number;
  // sayHello: () => void;
  sayHello(): void; // 对象内的函数 —— 方法
}

// // 使用type约束
// type User = {
//   name: string;
//   age: number;
//   sayHello: () => void;
// };

let u: User = {
  name: "sdfds",
  age: 33,
  sayHello() {
    console.log("asfadasfaf");
  },
};

2)使用接口约束函数

  • {} 是定界符
    • 不表示对象
    • 表示包裹的内容是具体的约束内容
// type Condition = (n: number) => boolean
// type Condition {
//   (n: number): boolean;
// }
interface Condition {
  (n: number): boolean;
}

// 对数组每一项作处理后求和
function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach((n) => {
    if (callBack(n)) {
      s += n;
    }
  });
  return s;
}

const result = sum([3, 4, 5, 7, 11], (n) => n % 2 !== 0);
console.log(result);

3.接口的继承

判断继承关系

只要可以被描述为 “A 是一个 B” 就构成继承关系

class Banner extends React.Component {}

1)组合多种接口

interface A {
  T1: string;
}
interface B {
  T2: number;
}
interface C extends A, B {
  T3: boolean;
}

let u: C = {
  T2: 33,
  T1: "43",
  T3: true,
};

2)使用类型别名

  • 可以实现类似的组合效果
  • 需要通过交叉类型 &
    • 连接多个类型
    • 结果类型必须包含所有交叉类型的属性和方法
type A = {
  T1: string;
};
type B = {
  T2: number;
};
type C = {
  T3: boolean;
} & A &
  B;

let u: C = {
  T2: 33,
  T1: "43",
  T3: true,
};

3)区别

  • 子接口不能覆盖父接口的成员
interface A {
  T1: string;
}
interface B {
  T2: number;
}
interface C extends A, B {
  // T1: number; // 报错
  T3: boolean;
}
  • 交叉类型会把相同成员的类型进行交叉
type A = {
  T1: string;
};
type B = {
  T2: number;
};
type C = {
  T1: number;
  T3: boolean;
} & A &
  B;

let u: C = {
  T2: 33,
  // T1: "43", // number & string,可以使用两种类型的方法,但是无法赋值
  T3: true,
};

4.readonly

  • 只读修饰符,修饰的目标是只读
  • 只读修饰符不出现在编译结果中

1)禁止不合理操作

// type User = {
interface User {
  readonly id: string;
  name: string;
  age: number;
}

let u: User = {
  id: "123",
  name: "Asdf",
  age: 33,
};

u.id = "32323"; // 不合理操作,初始化赋值后又尝试修改

2)修饰数组

interface User {
  // 第一个表示该属性不能重新赋值,第二个表示数组内容不可改变
  readonly arr: readonly string[];
}

let u: User = {
  arr: ["Sdf", "dfgdfg"],
};

// const 表示该数组不可重新赋值,readonly 表示该数组内容不可改变
// const arr: ReadonlyArray<number> = [3, 4, 6];
const arr: readonly number[] = [3, 4, 6];

// 修改原数组的函数都无法调用,包括根据索引修改
arr.push();
arr[0] = 3;

5.类型兼容性

  • 如果能完成 B->A 的赋值,则 B 和 A 类型兼容

1)总原则:鸭子辨型法

  • 又叫子结构辨型法
  • 目标类型需要某一些特征,赋值的类型只要能满足该特征即可
  • 为了让开发者使用类型时更丝滑
    • 比如第三方库的对象有多个属性,但是开发者只需要其中一两个属性
    • 有了该判定方法后就不需要约束整个第三方库的对象

2)基本类型

  • 赋值的类型需要完全匹配

3)对象类型

  • 赋值的类型使用鸭子辨型法判定
  • 当直接使用对象字面量赋值时,会进行更加严格的判断
  • 类型断言
    • 开发者非常清楚某个东西的类型,但是 TS 难以分辨
    • 可以通过类型断言 as 告诉 TS 确切类型
    • 数据 as 类型
interface Duck {
  sound: "嘎嘎嘎"; // 字面量
  swim(): void;
}
let person = {
  name: "伪装成鸭子的人",
  age: 11,
  sound: "嘎嘎嘎" as "嘎嘎嘎",
  swim() {
    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
  },
};

/**
 * person中有满足Duck类型的结构,所以可以赋值
 *    第三方变量不清楚自定义接口的结构
 *    所以赋值时宽松判断,允许不相关字段存在
 */
let duck: Duck = person;

/**
 * 直接使用对象字面量赋值,会进行更严格的判断
 *    直接书写字面量时一定清楚自定义接口的结构
 *    此时赋值严格判断,不允许不相关字段存在
 */
let duck2: Duck = {
  // name: "伪装成鸭子的人", // 报错
  // age: 11, // 报错
  sound: "嘎嘎嘎" as "嘎嘎嘎", // 类型断言
  swim() {
    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
  },
};

4)函数类型

  • 传递给目标函数的参数可以少,但不可以多
// 需求1:传递参数n,返回符合条件的数组
// 需求2:传递参数n和i,返回符合条件的数组中第i项
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;
}

/**
 * 缺少参数i,但是不报错
 */
const result = sum([3, 4, 5, 7, 11], (n) => n % 2 !== 0);
// const result = sum([3, 4, 5, 7, 11], (n, i) => i % 2 !== 0);
console.log(result);
  • 要求返回则必须返回,不要求返回则随意
    • 不要求返回的函数,说明内部不使用返回值
    • 尽管返回了也不会报错
// (value: number, index: number, array: number[]) => void
[34, 4].forEach((it) => console.log(it));

6.案例实操:使用接口优化扑克牌

  • 用接口改造程序,加入大小王

1)定义接口

// 一张扑克牌的接口
export interface NormalCard {
  color: Color;
  mark: Mark;
}
// 大小王的接口
export interface Joker {
  type: "big" | "small";
}
// 一副扑克牌的类型
export type Deck = (NormalCard | Joker)[];
  • 或者
// 一副扑克牌的类型
export interface Card {
  getString(): string;
}
// 一张扑克牌的接口
export interface NormalCard extends Card {
  color: Color;
  mark: Mark;
}
// 大小王的接口
export interface Joker extends Card {
  type: "big" | "small";
}
// 一副扑克牌的类型
export type Deck = Card[];

2)创建扑克牌

  • 方法一:使用字面量
export const createDeck = (): Deck => {
  const deck: Deck = [];
  const marks = Object.values(Mark);
  const colors = Object.values(Color);
  for (const m of marks) {
    for (const c of colors) {
      // // 字面量赋值要求更严格的检查
      // deck.push({
      //   color: c,
      //   mark: m,
      // });

      const card: NormalCard = {
        color: c,
        mark: m,
        getString() {
          return this.color + this.mark;
        },
      };
      deck.push(card);
    }
  }
  return deck;
};
  • 方法二:使用类型断言
export const createDeck = (): Deck => {
  const deck: Deck = [];
  const marks = Object.values(Mark);
  const colors = Object.values(Color);
  for (const m of marks) {
    for (const c of colors) {
      // // 写法一
      // deck.push({
      //   color: c,
      //   mark: m,
      //   getString() {
      //     return this.color + this.mark;
      //   },
      // } as Card);

      // 写法二
      // React 中不推荐,会和组件混淆
      deck.push(<Card>{
        color: c,
        mark: m,
        getString() {
          return this.color + this.mark;
        },
      });
    }
  }

  // 加入大小王
  let joker: Joker = {
    type: "small",
    getString() {
      return "jo";
    },
  };
  deck.push(joker);
  joker = {
    type: "big",
    getString() {
      return "JO";
    },
  };
  deck.push(joker);

  return deck;
};

3)打印扑克牌

export const printDeck = (deck: Deck) => {
  let result = "\n";
  deck.forEach((card, i) => {
    // let str = card.color + card.mark;
    // result += str + "\t";

    result += card.getString() + "\t";

    if ((i + 1) % 6 === 0) {
      result += "\n";
    }
  });
  console.log(result);
};

(七)TS 中的类

基础部分学习类的时候,仅讨论新增的语法部分

  • 面向对象思想
  • TS 类
    • 属性列表
    • 修饰符
      • 只读修饰符:readonly
      • 访问修饰符:public、private、protected

1.属性

  • 要求使用属性列表来描述类中的属性
// // JS写法,报错,声明类时不能在构造函数中动态增加属性
// class User {
//   constructor(name: string, age: number) {
//     this.name = name;
//     this.age = age;
//   }
// }

class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

1)属性的初始化检查

  • 严格检查属性是否初始化
    • 是否有构造函数
    • 是否在构造函数中赋值
    • 实例化对象时是否传递参数
"strictPropertyInitialization": true

2)属性的初始化位置

  • 构造函数中设置默认值
  • 属性列表设置默认值
class User {
  name: string;
  age: number;
  gender: "男" | "女" = "男"; // 默认值设置位置2

  // 默认值设置位置1
  // constructor(name: string, age: number, gender: "男" | "女" = "男") {
  //   this.name = name;
  //   this.age = age;
  //   this.gender = gender;
  // }

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

3)属性可以修饰为可选的

class User {
  name: string;
  age: number;
  gender: "男" | "女" = "男";

  // pid: string | null = null; // null 必须有默认值
  // pid: string | undefined;
  pid?: string;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

4)属性可以修饰为只读的

class User {
  name: string;
  age: number;
  gender: "男" | "女" = "男";
  pid?: string;
  readonly id: number; // 初始化之后不可改变

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.id = Math.random();
  }
}

2.访问修饰符

  • 可以控制类中的某个成员(属性和方法)的访问权限
  • 在 JS 中使用 Symble 实现私有成员

1)分类

修饰符含义
public公开的,所有的代码均可访问【默认】
private私有的,只有在类中可以访问
protected受保护的,只有父类和子类可以访问

2)属性使用访问修饰符

class User {
  name: string;
  age: number;
  gender: "男" | "女" = "男";
  pid?: string;
  readonly id: number;

  private _publishNumber: number = 3; // 每天一共可以发布多少篇文章
  private _curNumber: number = 0; // 当前可以发布的文章数量

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.id = Math.random();
  }

  // 发布文章
  publish(title: string) {
    if (this._curNumber < this._publishNumber) {
      console.log("发布一篇文章:" + title);
      this._curNumber++;
    } else {
      console.log("你今日发布的文章数量已达到上限");
    }
  }
}

3)属性简写

  • 如果某个属性是通过构造函数的参数传递的,并且不做任何处理的赋值给该属性
  • 可以进行简写,语法糖
  • 只需要在构造函数的参数前添加访问修饰符即可
class User {
  readonly id: number;

  constructor(
    public name: string,
    public age: number,
  ) {
    this.id = Math.random();
  }
}

3.访问器

  • 用于控制属性的读取和赋值
    • Vue 中的 computed 就是访问器
  • 通过访问器访问的属性最好设置为私有属性
    • 并且访问器内部的属性不能与访问器函数本身重名
    • 否则会死循环,调用栈溢出
  • 如果只声明了读取器,该属性自动变为只读属性
class User {
  readonly id: number;

  constructor(
    public name: string,
    private _age: number,
  ) {
    this.id = Math.random();
  }

  /**
   * 访问器
   */
  // 设置器
  // setAge(value: number) { // Java写法
  set age(value: number) {
    // C#写法
    if (value < 0) {
      this._age = 0;
    } else if (value > 200) {
      this._age = 200;
    } else {
      this._age = value;
    }
  }

  // 读取器
  // getAge() { // Java写法
  get age() {
    // C#写法
    return Math.floor(this._age);
  }
}

const u = new User("aa", 22);
u.age = 1.5;
console.log(u.age); // 1



 

































4.案例实操:使用类优化扑克牌

  • 用类改造程序

1)删除 utils,定义扑克牌类

// desck.ts
import { Mark, Color } from "./enums";
import { Card, Joker } from "./types";

// 一副扑克牌的类型
export class Deck {
  private cards: Card[] = [];

  // constructor() {
  //   this.init();
  // }
  constructor(cards?: Card[]) {
    if (cards) {
      this.cards = cards;
    } else {
      this.init();
    }
  }

  // 创建扑克牌
  private init() {
    const marks = Object.values(Mark);
    const colors = Object.values(Color);
    for (const m of marks) {
      for (const c of colors) {
        this.cards.push({
          color: c,
          mark: m,
          getString() {
            return this.color + this.mark;
          },
        } as Card);
      }
    }

    // 加入大小王
    let joker: Joker = {
      type: "small",
      getString() {
        return "jo";
      },
    };
    this.cards.push(joker);
    joker = {
      type: "big",
      getString() {
        return "JO";
      },
    };
    this.cards.push(joker);
  }

  // 打印扑克牌
  print() {
    let result = "\n";
    this.cards.forEach((card, i) => {
      result += card.getString() + "\t";
      if ((i + 1) % 6 === 0) {
        result += "\n";
      }
    });
    console.log(result);
  }
}

2)洗牌

// desck.ts
  shuffle() {
    for (let i = 0; i < this.cards.length; i++) {
      const targetIndex = this.getRandom(0, this.cards.length);
      const temp = this.cards[i];
      this.cards[i] = this.cards[targetIndex];
      this.cards[targetIndex] = temp;
    }
  }

  // 洗牌辅助函数(无法取到最大值)
  private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + min);
  }

3)发牌

  • 元组写法
// desck.ts
// // 结果有4个card[](元组),分别是三个玩家的牌和剩余的牌
// publish(): [Card[], Card[], Card[], Card[]] {
//   const result:[Card[], Card[], Card[], Card[]] = [[],[],[],[]];
// }
// 1个card[]就是一个Deck
publish(): [Deck, Deck, Deck, Deck] {
  let player1: Deck, player2: Deck, player3: Deck, left: Deck;
  player1 = this.takeCards(17);
  player2 = this.takeCards(17);
  player3 = this.takeCards(17);
  left = new Deck(this.cards);
  return [player1, player2, player3, left];
}

// 发牌辅助函数(从剩余牌堆中摸牌)
private takeCards(n: number): Deck {
  const cards: Card[] = [];
  for (let i = 0; i < n; i++) {
    cards.push(this.cards.shift() as Card);
  }
  return new Deck(cards);
}
  • 对象写法
interface PublishResult {
  player1: Deck;
  player2: Deck;
  player3: Deck;
  left: Deck;
}

publish(): PublishResult {
  let player1: Deck, player2: Deck, player3: Deck, left: Deck;
  player1 = this.takeCards(17);
  player2 = this.takeCards(17);
  player3 = this.takeCards(17);
  left = new Deck(this.cards);

  return {
    player1,
    player2,
    player3,
    left,
  };
}

4)主函数

import { Deck } from "./deck";

const deck = new Deck();
deck.shuffle();
console.log("======洗牌后======");
deck.print();
const res = deck.publish();
console.log("======发牌后======");

// console.log("======玩家1======");
// res[0].print();
// console.log("======玩家2======");
// res[1].print();
// console.log("======玩家3======");
// res[2].print();
// console.log("======剩余======");
// res[3].print();

console.log("===========玩家1========");
res.player1.print();
console.log("===========玩家2========");
res.player2.print();
console.log("===========玩家3========");
res.player3.print();
console.log("===========桌面========");
res.left.print();

(八)泛型

1.简介

1)问题引入

  • 书写某个函数时,会丢失一些类型信息
    • 比如参数、返回值、函数内部变量都需要同一个类型
    • 但是函数调用前不能确定该类型
  • 多个位置的类型应该保持一致或有关联的信息
function take(arr: any[], n: number): any[] {
  if (n >= arr.length) return arr;
  const newArr: any[] = []; // 类型推断为never,要显式标记any
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i]);
  }
  return newArr;
}
const newArr1 = take([1, 2, 3, 4], 2);
const newArr2 = take(["a", "b", "c", "d"], 2);

2)引入泛型

  • 指附属于函数、类、接口、类型别名之上的类型
  • 相当于是一个类型变量
    • 在定义时,无法预先知道具体的类型,可以用该变量来代替
    • 只有到调用时,才能确定它的类型
  • 很多时候,TS 会智能地根据传递的参数,推导出泛型的具体类型
    • 前提是参数使用了泛型
    • 如果无法完成推导,并且又没有传递具体的类型,默认为 空对象类型 never
  • 泛型可以 设置默认值
    • function take<T = number>(arr: T[], n: number): T[] {}
  • 泛型可以 解除某个功能和类型的耦合

2.在函数中使用泛型

  • 在函数名之后写上 <泛型名称>
function take<T>(arr: T[], n: number): T[] {
  if (n >= arr.length) return arr;
  const newArr: T[] = [];
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i]);
  }
  return newArr;
}
// const newArr1 = take<number>([1, 2, 3, 4], 2); // number[]
const newArr1 = take([1, 2, 3, 4], 2); // number[]
const newArr2 = take<string>(["a", "b", "c", "d"], 2); // string[]

3.在类型别名、接口、类中使用泛型

  • 直接在名称后写上 <泛型名称>

1)类型别名

// 回调函数,判断数组中的某一项是否满足条件
type callback<T> = (n: T, i: number) => boolean;

function filter<T>(arr: T[], callback: callback<T>): T[] {
  const newArr: T[] = [];
  arr.forEach((n, i) => {
    if (callback(n, i)) {
      newArr.push(n);
    }
  });
  return newArr;
}

const arr1 = [1, 2, 3, 444];
console.log(filter(arr1, (n) => n % 2 !== 0));

2)接口

interface callback<T> {
  (n: T, i: number): boolean;
}

3)类

export class ArrayHelper<T> {
  constructor(private arr: T[]) {}

  take(n: number): T[] {
    if (n >= this.arr.length) {
      return this.arr;
    }
    const newArr: T[] = [];
    for (let i = 0; i < n; i++) {
      newArr.push(this.arr[i]);
    }
    return newArr;
  }

  shuffle() {
    for (let i = 0; i < this.arr.length; i++) {
      const targetIndex = this.getRandom(0, this.arr.length);
      const temp = this.arr[i];
      this.arr[i] = this.arr[targetIndex];
      this.arr[targetIndex] = temp;
    }
  }

  private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + min);
  }
}

4.泛型约束

  • 用于现实泛型的取值
  • 约束泛型应该有某些属性或方法
interface hasNameProperty {
  name: string;
}

/**
 * 将某个对象的name属性的每个单词的首字母大小,然后将该对象返回
 */
function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
  obj.name = obj.name
    .split(" ")
    .map((s) => s[0].toUpperCase() + s.substring(1))
    .join(" ");
  return obj;
}

const o = {
  name: "kevin yuan",
  age: 22,
  gender: "男",
};
const newO = nameToUpperCase(o);
console.log(newO.name); // Kevin Yuan

5.多泛型

/**
 * 将两个数组进行混合
 * [1, 3, 4] + ["a", "b", "c"] = [1, "a", 3, "b", 4, "c"]
 */
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
  if (arr1.length != arr2.length) {
    throw new Error("两个数组长度不等");
  }
  let result: (T | K)[] = [];
  for (let i = 0; i < arr1.length; i++) {
    result.push(arr1[i]);
    result.push(arr2[i]);
  }
  return result;
}

const result = mixinArray([1, 3, 4], ["a", "b", "c"]);
result.forEach((r) => console.log(r)); // 1 a 3 b 4 c

6.案例实操:自定义字典类

  • 定义一个字典类(Dictionary)
  • 字典中会保存键值对的数据

1)键值对数据的特点

  • 键(key)可以是任何类型,但不允许重复
  • 值(value)可以是任何类型
  • 每个键对应一个值
  • 所有的键类型相同,所有的值类型相同

2)字典类中对键值对数据的操作

  • 按照键,删除对应的键值对
  • 循环每一个键值对
  • 得到当前键值对的数量
  • 判断某个键是否存在
  • 重新设置某个键对应的值,如果不存在,则添加

3)代码实现:字典类

interface Callback<K, V> {
  (k: K, v: V): void;
}

export class Dictionary<K, V> {
  private keys: K[] = [];
  private values: V[] = [];

  /**
   * 根据键,删除对应的键值对
   * @param key 待删除的键
   * @returns 是否删除成功
   */
  delete(key: K): boolean {
    const index = this.keys.indexOf(key);
    if (index === -1) return false;
    this.keys.splice(index, 1);
    this.values.splice(index, 1);
    return true;
  }

  /**
   * 循环键值对作处理
   * @param callback 遍历规则回调函数
   */
  each(callback: Callback<K, V>): void {
    this.keys.forEach((k, i) => {
      callback(k, this.values[i]);
    });
  }

  /**
   * 获取当前键值对的数量
   */
  get size() {
    return this.keys.length;
  }

  /**
   * 判断是否存在某个键
   * @param key 待查找的键
   */
  has(key: K): boolean {
    return this.keys.indexOf(key) !== -1;
  }

  /**
   * 设置键对应的值,不存在该键则添加
   * @param key 待设置的键
   * @param val 待设置的值
   */
  set(key: K, val: V): void {
    const index = this.keys.indexOf(key);
    if (index === -1) {
      this.keys.push(key);
      this.values.push(val);
    } else {
      this.values[index] = val;
    }
  }
}

4)代码实现:主函数

import { Dictionary } from "./Dictionary";

const dictionary = new Dictionary<string, number>();

dictionary.set("a", 1);
dictionary.set("b", 2);
dictionary.set("c", 3);

dictionary.each((k, v) => console.log(`${k}: ${v}`));
console.log(`当前字典的键值对数量为 ${dictionary.size}`);

console.log("删除键b");
dictionary.delete("b");

dictionary.each((k, v) => console.log(`${k}: ${v}`));
console.log(`当前字典的键值对数量为 ${dictionary.size}`);

console.log(`当前字典是否含有键a:${dictionary.has("a")}`);
console.log(`当前字典是否含有键b:${dictionary.has("b")}`);

相关信息

实际上 TS 中的字典类就是 Map 结构

(九)项目实战:使用 React + TS 开发三字棋游戏

三字棋游戏,又叫井字棋游戏

1.项目准备

facebook/create-react-appopen in new window

npx create-react-app 00reactgame --template typescript

1)tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "downlevelIteration": true, // 支持迭代器
    "allowJs": true, // 允许在ts文件中导入js,默认false
    "skipLibCheck": true, // 跳过对声明文件的检查
    "esModuleInterop": true, // 支持ES6标准默认导入commonjs,影响编译结果
    "allowSyntheticDefaultImports": true, // 支持ES6标准默认导入commonjs,只影响语法
    "strict": true, // 开启严格检查
    "forceConsistentCasingInFileNames": true, // 区分大小写文件的引入
    "module": "esnext", // 最新版本的模块化标准
    "moduleResolution": "node", // 模块解析方式
    "resolveJsonModule": true, // 允许解析json模块,导入后可以当对象使用
    "isolatedModules": true, // 每个文件都视作模块,即至少得有一个export default
    "noEmit": true, // 不生成编译的js文件,因为js文件还需要经过babel处理
    "jsx": "preserve" // 解析jsx的方式
  },
  "include": ["src"]
}

2)dom.iterable

  • 该库环境用于开启迭代器环境
    • 只会影响开发时的语法
    • 编译后遵循 target 配置项,编译为 ES5 语法
    • 所以不会有兼容性问题
  • ES6 的 Symble(符号)可以实现类的私有成员
  • ES6 提出了迭代器的概念
    • 表示数据可以被 for...of 循环,不是 for 循环
    • 该数据不一定是数组
  • 制作迭代器必须使用 Symble.Iterator
  • 旧版本浏览器没有迭代器的概念
    • 无法遍历 DOM 元素
    • 需要开启配置
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "downlevelIteration": true // 支持迭代器
  }
}



 
 


const doms = document.getElementsByTagName("button");
for (const dom of doms) {
}

3)"module": "esnext"

  • 使用 ESNext 最新版本的模块化标准
  • 无需担心兼容性问题
    • 旧版本要求 import 语句必须顶层声明
  • 编译流程:webpack -> ts -> js -> babel -> 最终结果
  • 在 ts 编译为 js 时都使用的新版本语法,但经过 babel 编译后会使用旧版本语法
  • 最后交回 webpack 打包

2.在 React 中使用 TS

1)使用 JS 开发时的问题

  • 难以确定某个组件有哪些属性需要传递
    • 查看官方文档
  • 难以确定每个属性应该传递什么类型
  • 传递事件时,有哪些参数
  • 错误发生在运行时
    • 可以通过 propTypes 约束属性的类型
    • 但是发生错误的时间点是在运行时

2)结合 TS 解决

  • TS 是一套静态的类型系统

3)React 组件分类

  • 展示组件
    • 通常是函数式组件
    • 只做页面展示
  • 容器组件
    • 通常是类组件
    • 只做数据处理

4)函数组件两种约束方式

import React from "react";

interface IProps {
  num: number;
  onChange?: (n: number) => void;
}

// 写法一
export function CountComp(props: IProps) {
  return (
    <div>
      <button
        onClick={() => {
          if (props.onChange) {
            props.onChange(props.num - 1);
          }
        }}
      >
        -
      </button>
      <span>{props.num}</span>
      <button
        onClick={() => {
          if (props.onChange) {
            props.onChange(props.num + 1);
          }
        }}
      >
        +
      </button>
    </div>
  );
}

// 写法二
export const CountComp: React.FC<IProps> = (props) => {
  return (
    <div>
      <button
        onClick={() => {
          if (props.onChange) {
            props.onChange(props.num - 1);
          }
        }}
      >
        -
      </button>
      <span>{props.num}</span>
      <button
        onClick={() => {
          if (props.onChange) {
            props.onChange(props.num + 1);
          }
        }}
      >
        +
      </button>
    </div>
  );
};
  • 写法二由于使用 React.FC 约束
  • 比写法一多了一个属性 props.children
  • 如果需要为接口的 可选参数 设置默认值,必须使用写法二
interface IProps {
  chesses: ChessType[];
  onClick?: (index: number) => void;
  isGameOver?: boolean;
}

export const BoardComp: React.FC<IProps> = ({ chesses, onClick, isGameOver }) => {
  return <div className="board">{list}</div>;
};

BoardComp.defaultProps = {
  isGameOver: false,
};



 


 






5)类组件的约束方式

警告

  • 如果要关联 state 和 setState 的约束
  • 必须在 React.Component 后声明泛型
interface IState {
  msg: string;
  desc: string;
}

export class CountComp extends React.Component<IProps, IState> {
  // 正常初始化会覆盖IState,需要显式约束state
  // state = {
  state: IState = {
    msg: "",
    desc: "",
    // test: 2
  };

  render() {
    // this.state.msg
    return (
      <div>
        <button
          onClick={() => {
            if (this.props.onChange) {
              this.props.onChange(this.props.num - 1);
            }
          }}
        >
          -
        </button>
        <span>{this.props.num}</span>
        <button
          onClick={() => {
            if (this.props.onChange) {
              this.props.onChange(this.props.num + 1);
            }
          }}
        >
          +
        </button>
      </div>
    );
  }
}

2.制作棋子组件

1)components/ChessComp.tsx

import React from "react";
import { ChessType } from "../types/enums";
import "./ChessComp.css";

interface IProps {
  type: ChessType;
  onClick?: () => void;
}

export function ChessComp({ type, onClick }: IProps) {
  let chess = null;
  if (type === ChessType.red) {
    chess = <div className="chess-item red"></div>;
  } else if (type === ChessType.black) {
    chess = <div className="chess-item black"></div>;
  }

  return (
    <div
      className="chess"
      onClick={() => {
        if (type === ChessType.none && onClick) {
          onClick();
        }
      }}
    >
      {chess}
    </div>
  );
}

2)App.tsx

import React from "react";
import { ChessComp } from "./components/ChessComp";
import { ChessType } from "./types/enums";

export class App extends React.Component {
  render() {
    return (
      <div>
        <ChessComp type={ChessType.none} onClick={() => console.log("被点击了")} />
        <ChessComp type={ChessType.red} onClick={() => console.log("被点击了")} />
        <ChessComp type={ChessType.black} onClick={() => console.log("被点击了")} />
      </div>
    );
  }
}

3.制作棋盘组件

1)components/BoardComp.tsx

import React from "react";
import { ChessType } from "../types/enums";
import { ChessComp } from "./ChessComp";
import "./BoardComp.css";

interface IProps {
  chesses: ChessType[];
  onClick?: (index: number) => void;
  isGameOver?: boolean;
}

export const BoardComp: React.FC<IProps> = ({ chesses, onClick, isGameOver }) => {
  const list = chesses.map((type, i) => (
    <ChessComp
      key={i}
      type={type}
      onClick={() => {
        onClick && !isGameOver && onClick(i);
      }}
    />
  ));
  return <div className="board">{list}</div>;
};

BoardComp.defaultProps = {
  isGameOver: false,
};

2)App.tsx

import React from "react";
import { BoardComp } from "./components/BoardComp";
import { ChessType } from "./types/enums";

const types: ChessType[] = [
  ChessType.none,
  ChessType.red,
  ChessType.black,
  ChessType.none,
  ChessType.red,
  ChessType.black,
  ChessType.none,
  ChessType.red,
  ChessType.black,
];

export class App extends React.Component {
  render() {
    return (
      <div>
        <BoardComp chesses={types} onClick={(i) => console.log(i)} isGameOver={true} />
      </div>
    );
  }
}

3)非空断言

  • 在数据后加上 !
  • 告诉 TS 不用考虑该数据为空的情况
export const BoardComp: React.FC<IProps> = ({ chesses, onClick, isGameOver }) => {
  /**
   * 设置了默认值的可选参数会判断为boolean | undefined
   * 实际上不可能为undefined,正常没有影响,涉及到计算时会有影响
   * 可以使用类型断言强制约束为boolean
   * 非空断言
   */
  // const isOver = isGameOver as boolean;
  const isOver = isGameOver!;

  const list = chesses.map((type, i) => (
    <ChessComp
      key={i}
      type={type}
      onClick={() => {
        onClick && !isOver && onClick(i);
      }}
    />
  ));
  return <div className="board">{list}</div>;
};

4.制作游戏组件

  • 提供并维护游戏中的数据
  • 有状态组件

1)App.tsx

import React from "react";
import { GameComp } from "./components/GameComp";

export class App extends React.Component {
  render() {
    return (
      <div>
        <GameComp />
      </div>
    );
  }
}

2)components/GameStatusComp.tsx

import React from "react";
import { ChessType, GameStatus } from "../types/enums";
import "./GameStatusComp.css";

interface IProps {
  status: GameStatus;
  next: ChessType.red | ChessType.black;
}

export function GameStatusComp(props: IProps) {
  let content: JSX.Element;
  if (props.status === GameStatus.gaming) {
    if (props.next === ChessType.red) {
      content = <div className="red">红方落子</div>;
    } else {
      content = <div className="black">黑方落子</div>;
    }
  } else {
    if (props.status === GameStatus.redWin) {
      content = <div className="win red">红方胜利</div>;
    } else if (props.status === GameStatus.blackWin) {
      content = <div className="win black">黑方胜利</div>;
    } else {
      content = <div className="win equal">平局</div>;
    }
  }

  return <div className="status">{content}</div>;
}

3)components/GameComp.tsx

import React from "react";
import { ChessType, GameStatus, NextChess } from "../types/enums";
import { BoardComp } from "./BoardComp";
import { GameStatusComp } from "./GameStatusComp";

interface IState {
  chesses: ChessType[];
  gameStatus: GameStatus;
  nextChess: ChessType.red | ChessType.black;
  // nextChess: NextChess;
}

export class GameComp extends React.Component<{}, IState> {
  state: IState = {
    chesses: [],
    gameStatus: GameStatus.gaming,
    nextChess: ChessType.black,
  };

  /**
   * 初始化数据
   */
  init() {
    const arr: ChessType[] = [];
    for (let i = 0; i < 9; i++) {
      arr.push(ChessType.none);
    }
    this.setState({
      chesses: arr,
      gameStatus: GameStatus.gaming,
      nextChess: ChessType.black,
    });
  }
  componentDidMount() {
    this.init();
  }

  /**
   * 处理棋子的点击事件
   * 执行时说明游戏没有结束且点击的位置没有棋子
   * @param index 棋子下标
   */
  handleChessClick(index: number) {
    const chesses: ChessType[] = [...this.state.chesses];
    chesses[index] = this.state.nextChess;
    this.setState((prevState) => ({
      chesses,
      nextChess: prevState.nextChess === ChessType.red ? ChessType.black : ChessType.red,
      gameStatus: this.getGameStatus(chesses, index),
    }));
  }

  /**
   * 获取当前游戏状态
   * @param chesses 点击后最新的棋子数组
   * @param index 最新落子的位置
   */
  getGameStatus(chesses: ChessType[], index: number): GameStatus {
    // 1.判断是否有一方获得胜利
    const horMin = Math.floor(index / 3) * 3;
    const verMin = Math.floor(index % 3);
    if (
      // 横向三连
      (chesses[horMin] === chesses[horMin + 1] && chesses[horMin] === chesses[horMin + 2]) ||
      // 纵向三连
      (chesses[verMin] === chesses[verMin + 3] && chesses[verMin] === chesses[verMin + 6]) ||
      // 斜向三连
      (chesses[0] === chesses[4] && chesses[0] === chesses[8] && chesses[0] !== ChessType.none) ||
      (chesses[2] === chesses[4] && chesses[2] === chesses[6] && chesses[2] !== ChessType.none)
    )
      return chesses[index] === ChessType.red ? GameStatus.redWin : GameStatus.blackWin;

    // 2.判断是否平局
    if (!chesses.includes(ChessType.none)) return GameStatus.equal;

    // 3.游戏正在进行
    return GameStatus.gaming;
  }

  render() {
    return (
      // 严格来说,容器组件不能有样式
      <div
        style={{
          textAlign: "center",
        }}
      >
        <h1>三连棋游戏</h1>
        <GameStatusComp status={this.state.gameStatus} next={this.state.nextChess} />
        <BoardComp chesses={this.state.chesses} isGameOver={this.state.gameStatus !== GameStatus.gaming} onClick={this.handleChessClick.bind(this)} />
        <button
          onClick={() => {
            this.init();
          }}
        >
          重新开始
        </button>
      </div>
    );
  }
}

4)types/enums.ts

export enum ChessType {
  none,
  red,
  black,
}

export enum NextChess {
  red = ChessType.red,
  black = ChessType.black,
}

export enum GameStatus {
  /**
   * 正在游戏中
   */
  gaming,
  /**
   * 红方胜利
   */
  redWin,
  /**
   * 黑方胜利
   */
  blackWin,
  /**
   * 平局
   */
  equal,
}
上次编辑于: