typescript学习笔记

介绍

  • 背景:JS 的类型系统存在“先天缺陷”弱类型,JS 代码中绝大部分错误都是类型错误(Uncaught TypeError)这些经常出现的错误,导致了在使用 JS 进行项目开发时,增加了找 Bug、改 Bug 的时间,严重影响开发效率

为什么会这样?

  • 从编程语言的动静来区分,TypeScript 属于静态类型的编程语言,JavaScript 属于动态类型的编程语言
    • 静态类型:编译期做类型检查
    • 动态类型:执行期做类型检查

代码编译和代码执行的顺序:1 编译 2 执行

  • 对于 JS 来说:需要等到代码真正去执行的时候才能发现错误(晚)
  • 对于 TS 来说:在代码编译的时候(代码执行前)就可以发现错误(早)

基础学习部分

安装

  • Node.js/浏览器,只认识 JS 代码,不认识 TS 代码。需要先将 TS 代码转化为 JS 代码,然后才能运行
npm i -g typescript 
// 或者 
yarn global add typescript

Mac 电脑安装全局包时,需要添加 sudo 获取权限:sudo npm i -g typescript* yarn 全局安装:sudo yarn global add typescript

tsc –v(查看 typescript 的版本)

数据类型

  1. JS 已有类型
  • 原始类型:number/string/boolean/null/undefined
  • 对象类型:object(包括,数组、对象、函数等对象)
  1. TS 新增类型
  • 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any 等

  • 注意:

    1. 原始类型在 TS 和 JS 中写法一致
    2. 对象类型在 TS 中更加细化,每个具体的对象(比如,数组、对象、函数)都有自己的类型语法
类型注解
let age = 18
let age: number = 18             // : number 就是类型注解
原始类型
  • 原始类型:number/string/boolean/null/undefined
  • 特点:简单,这些类型,完全按照 JS 中类型的名称来书写
let age: number = 18
let myName: string = '老师'
let isLoading: boolean = false

// 等等...
数组类型
// 写法一:
let numbers: number[] = [1, 3, 5] //数组中只允许number类型
// 写法二:
let strings: Array<string> = ['a', 'b', 'c'] //泛型写法,后面补充
联合类型
let arr: (number | string)[] = [1, 'a', 3, 'b']   // 数组中允许number和string类型
类型别名
  • 类型别名(自定义类型):为任意类型起别名
type CustomArray = (number | string)[]

let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
  • 解释:
    1. 使用 type 关键字来创建自定义类型
    2. 类型别名(比如,此处的 CustomArray)可以是任意合法的变量名称
    3. 推荐使用大写字母开头
    4. 创建类型别名后,直接使用该类型别名作为变量的类型注解即可
函数类型
  1. 基础概念
// 函数声明
function add(num1: number, num2: number): number {
  return num1 + num2
}

// 箭头函数
const add = (num1: number, num2: number): number => {
  return num1 + num2
}
  1. 类型别名方式
type AddFn = (num1: number, num2: number) => number

const add: AddFn = (num1, num2) => {
  return num1 + num2
}
  1. void类型

    • 如果函数没有返回值,那么,函数返回值类型为:void
  2. 可选参数

function mySlice(start?: number, end?: number): void {
  console.log('起始索引:', start, '结束索引:', end)
}
  1. 参数默认值
let func3 = (a: number = 1): number =>  a
对象类型
  1. 基本概念
// 空对象
let person: {} = {}

// 有属性的对象
let person: { name: string } = {
  name: '同学'
}

// 既有属性又有方法的对象
// 在一行代码中指定对象的多个属性类型时,使用 `;`(分号)来分隔
let person: { name: string; sayHi(): void } = {
  name: 'jack',
  sayHi() {}
}

// 对象中如果有多个类型,可以换行写:
// 通过换行来分隔多个属性类型,可以去掉 `;`
let person: {
  name: string
  sayHi(): void
} = {
  name: 'jack',
  sayHi() {}
}
  1. 对象可选属性
let obj: {
  name: string;
  age?: number;
  do: (what: string) => string;
  eat?(what1: string): string
} = {
  name: 'glm',
  do: (what) => {
    return what;
  }
};
  1. 类型别名
// 创建类型别名
type Person = {
  name: string
  sayHi(): void
}

// 使用类型别名作为对象的类型:
let person: Person = {
  name: 'jack',
  sayHi() {}
}
  1. 接口
  • 当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的
interface IPerson {
  name: string
  age: number
  sayHi(): void
}

let person: IPerson = {
  name: 'jack',
  age: 19,
  sayHi() {}
}
  1. interface vs type
  • 相同点:都可以给对象指定类型
  • 不同点:
    1. 接口,只能为对象指定类型
    2. 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名
  • 推荐:能使用 type 就是用 type
  1. 接口继承
  • 如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用
  • 比如,这两个接口都有 x、y 两个属性,重复写两次,可以,但很繁琐
interface Point2D { x: number; y: number }
interface Point3D { x: number; y: number; z: number }
  • 更好的方式:
interface Point2D { x: number; y: number }
// 继承 Point2D
interface Point3D extends Point2D {
  z: number
}
元组类型
  • 场景:在地图中,使用经纬度坐标来标记位置信息
  • 可以使用数组来记录坐标,那么,该数组中只有两个元素,并且这两个元素都是数值类型
let position: number[] = [116.2317, 39.5427]
  • 使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字
  • 更好的方式:元组 Tuple
  • 元组类型是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型
let position: [number, number] = [39.5427, 116.2317]
类型推论
// 变量 age 的类型被自动推断为:number
let age = 18

// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number) {
  return num1 + num2
}
字面量类型
  1. 基本使用
  • 思考以下代码,两个变量的类型分别是什么?
let str1 = 'Hello TS'
const str2 = 'Hello TS'
  • 通过 TS 类型推论机制,可以得到答案:

    1. 变量 str1 的类型为:string
    2. 变量 str2 的类型为:‘Hello TS’
  • 解释:

  1. str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string
  2. str2 是一个常量(const),它的值不能变化只能是 ‘Hello TS’,所以,它的类型为:‘Hello TS’
  • 注意:此处的 ‘Hello TS’,就是一个字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型
  • 任意的 JS 字面量(比如,对象、数字等)都可以作为类型使用
    • 字面量:{ name: 'jack' } [] 18 20 'abc' false function() {}
  1. 使用场景
  • 使用模式:字面量类型配合联合类型一起使用
  • 使用场景:用来表示一组明确的可选值列表
  • 比如,在贪吃蛇游戏中,游戏的方向的可选值只能是上、下、左、右中的任意一个
// 使用自定义类型:
type Direction = 'up' | 'down' | 'left' | 'right'

function changeDirection(direction: Direction) {
  console.log(direction)
}

// 调用函数时,会有类型提示:
changeDirection('up')
  • 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个
  • 优势:相比于 string 类型,使用字面量类型更加精确、严谨
枚举类型
// Down -> 11、Left -> 12、Right -> 13
enum Direction { Up = 10, Down, Left, Right }

enum Direction { Up = 2, Down = 4, Left = 8, Right = 16 }
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT'
}
any 类型
  • 原则:不推荐使用 any!这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)
  • 因为当值的类型为 any 时,可以对该值进行任意操作,并且不会有代码提示
let obj: any = { x: 0 }

obj.bar = 100
obj()
const n: number = obj
  • 解释:以上操作都不会有任何类型错误提示,即使可能存在错误
  • 尽可能的避免使用 any 类型,除非临时使用 any 来“避免”书写很长、很复杂的类型
  • 其他隐式具有 any 类型的情况
    1. 声明变量不提供类型也不提供默认值
    2. 函数参数不加类型
  • 注意:因为不推荐使用 any,所以,这两种情况下都应该提供类型
类型断言
const aLink = document.getElementById('link') as HTMLAnchorElement

泛型

function id<T>(value: T): T { return value }
// 调用泛型函数
const num = id<number>(10)
const str = id<string>('a')
泛型接口
interface IdFunc<Type> {
  id: (value: Type) => Type
  ids: () => Type[]
}

let obj: IdFunc<number> = {
  id(value) { return value },
  ids() { return [1, 3, 5] }
}
泛型工具类型
Partial
  • Partial 用来构造(创建)一个类型,将 Type 的所有属性设置为可选。
type Props =  {
  id: string
  children: number[]
}

type PartialProps = Partial<Props>
  • 解释:构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的。
Readonly
  • Readonly 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)。
type Props =  {
  id: string
  children: number[]
}

type ReadonlyProps = Readonly<Props>
  • 解释:构造出来的新类型 ReadonlyProps 结构和 Props 相同,但所有属性都变为只读的。
let props: ReadonlyProps = { id: '1', children: [] }
// 错误演示
props.id = '2'
  • 当我们想重新给 id 属性赋值时,就会报错:无法分配到 “id” ,因为它是只读属性。
Pick
  • Pick<Type, Keys> 从 Type 中选择一组属性来构造新类型。
interface Props {
  id: string
  title: string
  children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>

项目中使用

创建新项目

  • 命令:npx create-react-app my-app --template typescript

  • 说明:在命令行中,添加 --template typescript 表示创建支持 TS 的项目

  • 项目目录的变化:

    1. 在项目根目录中多了一个文件:tsconfig.json
      • TS 的配置文件
    2. 在 src 目录中,文件的后缀有变化,由原来的 .js 变为 .ts.tsx
      • .ts ts 文件的后缀名
      • .tsx 是在 TS 中使用 React 组件时,需要使用该后缀,只要文件中使用了jsx模板,后缀名必须叫tsx
    3. 在 src 目录中,多了 react-app-env.d.ts 文件
      • .d.ts 类型声明文件,用来指定类型

tsconfig的介绍

  • tsconfig.json是typescript项目的配置文件,用于配置typescript
  • tsconfig.json配置文件可以通过 tsc --init 生成
  • 说明:所有的配置项都可以通过鼠标移入的方式,来查看配置项的解释说明。
  • tsconfig 文档链接
{
  // 编译选项
  "compilerOptions": {
    // 生成代码的语言版本:将我们写的 TS 代码编译成哪个版本的 JS 代码
    // 命令行: tsc --target es5 11-测试TS配置文件.ts
    "target": "es5",
    // 指定要包含在编译中的 library
    "lib": ["dom", "dom.iterable", "esnext"],
    // 允许 ts 编译器编译 js 文件
    "allowJs": true,
    // 跳过类型声明文件的类型检查
    "skipLibCheck": true,
    // es 模块 互操作,屏蔽 ESModule 和 CommonJS 之间的差异
    "esModuleInterop": true,
    // 允许通过 import x from 'y' 即使模块没有显式指定 default 导出
    "allowSyntheticDefaultImports": true,
    // 开启严格模式
    "strict": true,
    // 对文件名称强制区分大小写  Demo.ts  
    "forceConsistentCasingInFileNames": true,
    // 为 switch 语句启用错误报告
    "noFallthroughCasesInSwitch": true,
    // 生成代码的模块化标准
    "module": "esnext",
    // 模块解析(查找)策略
    "moduleResolution": "node",
    // 允许导入扩展名为.json的模块
    "resolveJsonModule": true,
    // 是否将没有 import/export 的文件视为旧(全局而非模块化)脚本文件
    "isolatedModules": true,
    // 编译时不生成任何文件(只进行类型检查)
    "noEmit": true,
    // 指定将 JSX 编译成什么形式
    "jsx": "react-jsx"
  },
  // 指定允许 ts 处理的目录
  "include": ["src"]
}

类型声明文件-基本介绍

今天几乎所有的 JavaScript 应用都会引入许多第三方库来完成任务需求。

这些第三方库不管是否是用 TS 编写的,最终都要编译成 JS 代码,才能发布给开发者使用。

我们知道是 TS 提供了类型,才有了代码提示和类型保护等机制。

但在项目开发中使用第三方库时,你会发现它们几乎都有相应的 TS 类型,这些类型是怎么来的呢? 类型声明文件

  • 类型声明文件:用来为已存在的 JS 库提供类型信息

  • TS 中有两种文件类型:1 .ts 文件 2 .d.ts 文件

  • .ts 文件:

    1. 既包含类型信息又可执行代码
    2. 可以被编译为 .js 文件,然后,执行代码
    3. 用途:编写程序代码的地方
  • .d.ts 文件:

    1. 只包含类型信息的类型声明文件
    2. 不会生成 .js 文件,仅用于提供类型信息,在.d.ts文件中不允许出现可执行的代码,只用于提供类型
    3. 用途:为 JS 提供类型信息
  • 总结:.ts 是 implementation(代码实现文件);.d.ts 是 declaration(类型声明文件)

  • 如果要为 JS 库提供类型信息,要使用 .d.ts 文件

类型声明文件-内置文件

  • TS 为 JS 运行时可用的所有标准化内置 API 都提供了声明文件
  • 比如,在使用数组时,数组所有方法都会有相应的代码提示以及类型信息:
const strs = ['a', 'b', 'c']
// 鼠标放在 forEach 上查看类型
strs.forEach
  • 实际上这都是 TS 提供的内置类型声明文件
  • 可以通过 Ctrl + 鼠标左键(Mac:Command + 鼠标左键)来查看内置类型声明文件内容
  • 比如,查看 forEach 方法的类型声明,在 VSCode 中会自动跳转到 lib.es5.d.ts 类型声明文件中
  • 当然,像 window、document 等 BOM、DOM API 也都有相应的类型声明(lib.dom.d.ts)

类型声明文件-第三方库

  • 目前,几乎所有常用的第三方库都有相应的类型声明文件
  • 第三方库的类型声明文件有两种存在形式:1 库自带类型声明文件 2 由 DefinitelyTyped 提供。
  1. 库自带类型声明文件:比如,axios
  • 查看 node_modules/axios 目录

解释:这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明。

  1. 由 DefinitelyTyped 提供
  • DefinitelyTyped 是一个 github 仓库,用来提供高质量 TypeScript 类型声明
  • DefinitelyTyped 链接
  • 可以通过 npm/yarn 来下载该仓库提供的 TS 类型声明包,这些包的名称格式为:@types/*
  • 比如,@types/react、@types/lodash 等
  • 说明:在实际项目开发时,如果你使用的第三方库没有自带的声明文件,VSCode 会给出明确的提示
import _ from 'lodash'

// 在 VSCode 中,查看 'lodash' 前面的提示
  • 解释:当安装 @types/* 类型声明包后,TS 也会自动加载该类声明包,以提供该库的类型声明
  • 补充:TS 官方文档提供了一个页面,可以来查询 @types/* 库
  • @types/* 库

类型声明文件-自定义

项目内共享类型

  • 如果多个 .ts 文件中都用到同一个类型,此时可以创建 .d.ts 文件提供该类型,实现类型共享。
  • 操作步骤:
    1. 创建 index.d.ts 类型声明文件。
    2. 创建需要共享的类型,并使用 export 导出(TS 中的类型也可以使用 import/export 实现模块化功能)。
    3. 在需要使用共享类型的 .ts 文件中,通过 import 导入即可(.d.ts 后缀导入时,直接省略)。

为已有 JS 文件提供类型声明

  1. 在将 JS 项目迁移到 TS 项目时,为了让已有的 .js 文件有类型声明。
  2. 成为库作者,创建库给其他人使用。
  • 演示:基于最新的 ESModule(import/export)来为已有 .js 文件,创建类型声明文件。

类型声明文件的使用说明

  • 说明:TS 项目中也可以使用 .js 文件。
  • 说明:在导入 .js 文件时,TS 会自动加载与 .js 同名的 .d.ts 文件,以提供类型声明。
  • declare 关键字:用于类型声明,为其他地方(比如,.js 文件)已存在的变量声明类型,而不是创建一个新的变量。
    1. 对于 type、interface 等这些明确就是 TS 类型的(只能在 TS 中使用的),可以省略 declare 关键字。
    2. 对于 let、function 等具有双重含义(在 JS、TS 中都能用),应该使用 declare 关键字,明确指定此处用于类型声明。
let count = 10
let songName = '痴心绝对'
let position = {
  x: 0,
  y: 0
}

function add(x, y) {
  return x + y
}

function changeDirection(direction) {
  console.log(direction)
}

const fomartPoint = point => {
  console.log('当前坐标:', point)
}

export { count, songName, position, add, changeDirection, fomartPoint }

定义类型声明文件

declare let count:number

declare let songName: string

interface Position {
  x: number,
  y: number
}

declare let position: Position

declare function add (x :number, y: number) : number

type Direction = 'left' | 'right' | 'top' | 'bottom'

declare function changeDirection (direction: Direction): void

type FomartPoint = (point: Position) => void

declare const fomartPoint: FomartPoint

export {
  count, songName, position, add, changeDirection, FomartPoint, fomartPoint
}

React与Typescript

useState的使用

**目标:**掌握useState hooks配合typescript使用

内容:

  • useState接收一个泛型参数,用于指定初始值的类型
  • useState的源码如下
/**
 * Returns a stateful value, and a function to update it.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#usestate
 */
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
  • useState的使用
const [name, setName] = useState<string>('张三')
const [age, setAge] = useState<number>(28)
const [isProgrammer, setIsProgrammer] = useState<boolean>(true)

// 如果你在set函数中的参数不符合声明的变量类型,程序会报错
<button onClick={() => setName(100)}>按钮</button>  // 报错
  • useState的类型推断,在使用useState的时候,只要提供了初始值,typescript会自动根据初始值进行类型推断,因此useState的泛型参数可以省略
export default function App() {
  const [name, setName] = useState('张三')
  const [age, setAge] = useState(28)
  const [isProgrammer, setIsProgrammer] = useState(true)
  return (
    <div>
      <button onClick={() => setName(100)}>按钮</button>
    </div>
  )
}

useEffect的使用

**目标:**掌握useEffect hook在typescript中的使用

内容

  • useEffect是用于我们管理副作用(例如 API 调用)并在组件中使用 React 生命周期的
  • useEffect的源码
/**
 * Accepts a function that contains imperative, possibly effectful code.
 *
 * @param effect Imperative function that can return a cleanup function
 * @param deps If present, effect will only activate if the values in the list change.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#useeffect
 */
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
  • useEffect函数不涉及到任何泛型参数,在typescript中使用和javascript中使用完全一致。
useEffect(() => {
  // 给 window 绑定点击事件
  const handleClick = () => {
    console.log('哈哈哈')
  }
  window.addEventListener('click', handleClick)

  return () => {
    // 给 window 移除点击事件
    window.addEventListener('click', handleClick)
  }
}, [])

useState 进阶用法

**目标:**能够使用useEffect发送请求并且配合useState进行渲染

内容:

  • 频道列表接口:http://geek.itheima.net/v1_0/channels

  • 需求,发送请求获取频道列表数据,并且渲染

  • **注意:**useState如果没有提供具体类型的初始值,是需要使用泛型参数指定类型的。

// 存放频道列表数据
// 如果给useState的泛型参数直接指定为一个[],那将会得到一个never类型的数据,渲染的时候会出问题
const [list, setList] = useState([])

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryj7Gmzz-1652862937632)(images/image-20211121180010600.png)]

  • 如果useState的初始值是一个复杂的数据类型,需要给useState指定泛型参数
import { useEffect, useState } from 'react'
import axios from 'axios'
type Res = {
  id: number
  name: string
}[]
export default function App() {
  // 存放频道列表数据
  const [list, setList] = useState<Res>([])
  useEffect(() => {
    const fetchData = async () => {
      const res = await axios.get('http://geek.itheima.net/v1_0/channels')
      setList(res.data.data.channels)
    }
    fetchData()
  }, [])
  return (
    <div>
      <ul>
        {list.map((item) => {
          return <li key={item.id}>{item.name}</li>
        })}
      </ul>
    </div>
  )
}

useRef的使用

**目标:**能够使用useRef配合ts操作DOM

内容:

  • useRef 接收一个泛型参数,源码如下
/**
 * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
 * (`initialValue`). The returned object will persist for the full lifetime of the component.
 *
 * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
 * value around similar to how you’d use instance fields in classes.
 *
 * @version 16.8.0
 * @see https://reactjs.org/docs/hooks-reference.html#useref
 */
function useRef<T>(initialValue: T): MutableRefObject<T>;
    
interface MutableRefObject<T> {
    current: T;
}
  • useRef的泛型参数用于指定current属性的值的类型

  • 如果使用useRef操作DOM,需要明确指定所操作的DOM的具体的类型,否则current属性会是null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XLStKS8K-1652862937635)(images/image-20211121181806382.png)]

  • 正确语法:
const inputRef = useRef<HTMLInputElement>(null)
const get = () => {
  console.log(inputRef.current?.value)
}
  • **技巧:**如何获取一个DOM对象的类型,鼠标直接移动到该元素上,就会显示出来该元素的类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLNyw3fa-1652862937636)(images/image-20211121182048771.png)]

可选链操作符

**目标:**掌握js中的提供的可选链操作符语法

内容

  • 可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。
  • 参考文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Optional_chaining
let nestedProp = obj.first?.second;
console.log(res.data?.data)
obj.fn?.()

if (obj.fn) {
    obj.fn()
}
obj.fn && obj.fn()

// 等价于
let temp = obj.first;
let nestedProp = ((temp === null || temp === undefined) ? undefined : temp.second);

非空断言

**目标:**掌握ts中的非空断言的使用语法

内容:

  • 如果我们明确的知道对象的属性一定不会为空,那么可以使用非空断言 !
// 告诉typescript, 明确的指定obj不可能为空
let nestedProp = obj!.second;
  • 注意:非空断言一定要确保有该属性才能使用,不然使用非空断言会导致bug

react路由的使用

**目标:**能够在typescript中使用react路由

内容:

  • 安装react-router-dom的类型声明文件yarn add @types/react-router-dom
  • 新建组件Home.tsxLogin.tsx
  • 配置路由
import { BrowserRouter as Router, Link, Route } from 'react-router-dom'
import Home from './pages/Home'
import Login from './pages/Login'
export default function App() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/home">首页</Link>
          </li>
          <li>
            <Link to="/login">登录页</Link>
          </li>
        </ul>
        <div>
          <Route path="/home" component={Home}></Route>
          <Route path="/login" component={Login}></Route>
        </div>
      </div>
    </Router>
  )
}

  • **注意:**有了ts的支持后,代码提示变得非常的精确

useHistory的使用

**目标:**掌握useHistory在typescript中的使用

内容:

  • useHistory可以实现路由之间的跳转,并且在跳转时可以指定跳转参数state的类型

  • useHistory的源码如下

export function useHistory<HistoryLocationState = H.LocationState>(): H.History<HistoryLocationState>;
  • useHistory如果仅仅实现跳转功能,和js中使用语法一致
const history = useHistory()
const login = () => {
  history.push('/login')
}
  • useHistory可以通过泛型参数来指定state的类型
const history = useHistory<{
  aa: string
}>()
const login = () => {
  history.push({
    pathname: '/login',
    state: {
      aa: 'cc',
    },
  })
}

useLocation的使用

**目标:**掌握useLocation在typescript中的使用

内容:

  • useLocation接收一个泛型参数,用于指定接收的state类型,与useHistory的泛型参数对应
  • useLocation的源码
export function useLocation<S = H.LocationState>(): H.Location<S>;
  • 基本使用
import { useLocation } from 'react-router'

export default function Home() {
  const location = useLocation<{ aa: string } | null>()
  const aa = location.state?.aa

  return <div>Home组件---{aa}</div>
}

**注意:**因为useLocation和useHistory都需要指定Location类型,因此可以将类型存放到通用的类型声明文件中

// types.d.ts
export type LoginState = {
  aa: string
} | null

useParams的使用

目标:能够掌握useParams在typescript中的使用

内容:

  • useParams接收一个泛型参数,用于指定params对象的类型
  • 基本使用
import { useParams } from 'react-router'

export default function Article() {
  const params = useParams<{ id: string }>()
  console.log(params.id)

  return (
    <div>
      文章详情
      <div>12</div>
    </div>
  )
}

unkonw类型

**目标:**了解什么是TS中的unknown类型

内容:

  • unknown是更加安全的any类型。
  • 我们可以对 any 进行任何操作,不需要检查类型。
// 没有类型检查就没有意义了,跟写JS一样。很不安全。
let value:any
value = true
value = 1
value.length
  • 也可以把任何值赋值给 unknown,但是不能调用属性和方法,除非使用类型断言或者类型收窄
let value:unknown
value = 'abc'

(value as string).length

if (typeof value === 'string') {
  value.length
}

redux基本使用

**目标:**掌握在ts项目中如何初始化redux

内容:

  • 安装依赖包
yarn add redux react-redux redux-devtools-extension
  • 新建文件 store/index.ts
import { createStore } from 'redux'
import reducer from './reducers'
import { composeWithDevTools } from 'redux-devtools-extension'
const store = createStore(reducer, composeWithDevTools())

export default store

  • 新建文件 store/reducers/index.ts
import { combineReducers } from 'redux'
import todos from './todos'
const rootReducer = combineReducers({
  todos,
})
export default rootReducer

  • 新建文件 store/reducers/todos.ts
const initValue = [
  {
    id: 1,
    name: '吃饭',
    done: false,
  },
  {
    id: 2,
    name: '睡觉',
    done: true,
  },
  {
    id: 3,
    name: '打豆豆',
    done: false,
  },
]
export default function todos(state = initValue, action: any) {
  return state
}

  • index.tsx中
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

useSelector的使用

**目标:**掌握useSelector在ts中的使用

内容

  • useSelector接收两个泛型参数
    • 类型变量TState用于指定state的类型
    • TSelected用于指定返回值的类型
export function useSelector<TState = DefaultRootState, TSelected = unknown>(
    selector: (state: TState) => TSelected,
    equalityFn?: (left: TSelected, right: TSelected) => boolean
): TSelected;
  • useSelector的基本使用
// 获取todos数据
const todos = useSelector<{ name: string }, string>((state) => state.name)
  • useSelector使用方式2,不指定泛型参数,直接指定state的类型
  • 参考文档:https://react-redux.js.org/using-react-redux/usage-with-typescript#typing-the-useselector-hook
const todos = useSelector((state: { name: string }) => state.name)

问题:如何准确的获取到store的类型??

RootState获取

**目标:**能够掌握如何获取redux的rootState

内容:

  • 参考文档:https://react-redux.js.org/using-react-redux/usage-with-typescript

  • typeof可以获取某个数据的类型

function fn(n1: number, n2:number):number {
  return n1 + n2
}

// 获取fn函数的类型
type Fn = typeof fn
  • ReturnType是一个泛型工具类型,可以获取一个函数类型的返回值类型
function fn(n1: number, n2:number):number {
  return n1 + n2
}

// 获取fn函数的类型
type Fn = typeof fn

// 获取Fn函数的返回值类型
type Res = ReturnType<Fn>
  • 获取RootState的操作 store/index.tx
export type RootState = ReturnType<typeof store.getState>
  • **重要:**useSelector的正确用法
import { RootState } from '../store'

// 获取todos数据
const todos = useSelector((state: RootState) => state.todos)

reducer的使用

**目标:**掌握reducers在TS中的写法

内容

  • 准备Action
export function addTodo(name: string) {
  return {
    type: 'ADD_TODO',
    name,
  }
}

export function delTodo(id: number) {
  return {
    type: 'DEL_TODO',
    id,
  }
}

  • 需要给action提供类型
export type TodoAction =
  | {
      type: 'ADD_TODO'
      name: string
    }
  | {
      type: 'DEL_TODO'
      id: number
    }

export const addTodo = (name: string): TodoAction => {
  return {
    type: 'ADD_TODO',
    name
  }
}

export const delTodo = (id: number): TodoAction => {
  return {
    type: 'DEL_TODO',
    id
  }
}

  • 在reducer中指定初始值的类型
type TodosList = {
  id: number
  name: string
  done: boolean
}[]
const initValue: TodosList = []
export default function todos(state = initValue, action: any): TodosList {
  return state
}


  • 指定Action的类型
import { TodoAction } from '../actions/todos'
  • 编写reducer
import { TodoAction } from '../types'
export default function todos(
  state = initValue,
  action: TodoAction
): TodosList {
  if (action.type === 'ADD_TODO') {
    return [
      {
        id: Date.now(),
        name: action.name,
        done: false,
      },
      ...state,
    ]
  }
  if (action.type === 'DEL_TODO') {
    return state.filter((item) => item.id !== action.id)
  }
  return state
}

useDispatch的使用

**目标:**掌握useDispatch在ts中的使用

内容:

  • useDispatch接收一个泛型参数用于指定Action的类型
  • 参考链接:https://react-redux.js.org/using-react-redux/usage-with-typescript#typing-the-usedispatch-hook
const dispatch = useDispatch()

<button onClick={() => dispatch(delTodo(item.id))}>x</button>

事件对象的类型

**目标:**掌握事件对象在TS中如何指定类型

内容:

  • 在使用事件对象时,需要指定事件对象的类型
const add = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.code === 'Enter') {
    dispatch(addTodo(name))
    setName('')
  }
}
  • 技巧:在行内事件中,鼠标移动到e上面可以看到具体的事件对象类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmnCdEVO-1652862937637)(images/image-20211123215525876.png)]

redux thunk的使用

**目标:**掌握redux thunk在typescript中的使用

内容:

  • 引入redux-thunk
yarn add redux-thunk

import thunk from 'redux-thunk'
const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
  • thunk类型的变更,使用了thunk之后,返回的Action类型不再是对象,而是函数类型的Action,因此需要修改Action的类型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3elGgOL5-1652862937638)(images/image-20211123221615977.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dfSAA49F-1652862937638)(images/image-20211123221710008.png)]

  • ThunkAction类型的使用
    • 参考文档:https://redux.js.org/usage/usage-with-typescript#type-checking-redux-thunks
export type RootThunkAction = ThunkAction<void, RootState, unknown, TodoAction>

// 修改删除Action
export function delTodo(id: number): RootThunkAction {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({
        type: 'DEL_TODO',
        id,
      })
    }, 1000)
  }
}

redux-thunk版本bug

  • 在redux-thunk@2.4.0新版中,使用dispatch的时候,会丢失提示,需要降级到2.3.0版本

  • https://github.com/reduxjs/redux-thunk/issues/326

  • yarn add redux-thunk@2.3.0

综合案例

项目搭建

引入通用样式(资料中已经准备好)

import './styles/index.css'

封装频道组件和新闻列表组件

components/Channel.js

import React from 'react'

export default function Channel() {
  return (
    <ul className="catagtory">
      <li className="select">开发者资讯</li>
      <li>ios</li>
      <li>c++</li>
      <li>android</li>
      <li>css</li>
      <li>数据库</li>
      <li>区块链</li>
      <li>go</li>
      <li>产品</li>
      <li>后端</li>
      <li>linux</li>
      <li>人工智能</li>
      <li>php</li>
      <li>javascript</li>
      <li>架构</li>
      <li>前端</li>
      <li>python</li>
      <li>java</li>
      <li>算法</li>
      <li>面试</li>
      <li>科技动态</li>
      <li>js</li>
      <li>设计</li>
      <li>数码产品</li>
      <li>html</li>
      <li>软件测试</li>
      <li>测试开发</li>
    </ul>
  )
}

components/NewsList.js

import React from 'react'
import avatar from '../assets/back.jpg'
export default function NewsList() {
  return (
    <div className="list">
      <div className="article_item">
        <h3 className="van-ellipsis">python数据预处理 :数据标准化</h3>
        <div className="img_box">
          <img src={avatar} className="w100" alt="" />
        </div>
        <div className="info_box">
          <span>13552285417</span>
          <span>0评论</span>
          <span>2018-11-29T17:02:09</span>
        </div>
      </div>
    </div>
  )
}

根组件中渲染

import React from 'react'
import Channel from './components/Channel'
import NewsList from './components/NewsList'
export default function App() {
  return (
    <div className="app">
      <Channel></Channel>
      <NewsList></NewsList>
    </div>
  )
}

准备静态资源

接口说明

获取频道列表

http://geek.itheima.net/v1_0/channels

获取频道新闻

http://geek.itheima.net/v1_0/articles?channel_id=频道id&timestamp=时间戳

加载频道数据

步骤:

在store/reducers/channel.ts

const initValue = {
  channelList: [],
  active: 0
}

export default function channel(state = initValue, action: any) {
  return state
}

在store/actions/channel.ts

import axios from 'axios'
import { RootThunkAction } from '..'

export function getChannelList(): RootThunkAction {
  return async (dispatch) => {
    const res = await axios.get('http://geek.itheima.net/v1_0/channels')
    console.log(res)
  }
}

在components/Channel.tsx中

export default function Channel() {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(getChannelList())
  }, [dispatch])

把频道数据存储到redux

在store/actions/channel.ts

import axios from 'axios'
import { RootThunkAction } from '..'

// 1. 提供了Channel的类型
export type Channel = {
  id: number
  name: string
}

// 2. 提供了ChannelAction的类型
export type ChannelAction = {
  type: 'channel/getChannelList'
  payload: Channel[]
}
export function getChannelList(): RootThunkAction {
  return async (dispatch) => {
    const res = await axios.get('http://geek.itheima.net/v1_0/channels')
    
    // 3. dispatch ChannelAction  问题: dispatch的时候没有提示
    dispatch({
      type: 'channel/getChannelList',
      payload: res.data.data.channels
    })
  }
}

在store/index.ts

import { TodoAction } from './actions/todos'
import { ChannelAction } from './actions/channel'

export type RootAction = TodoAction | ChannelAction
export type RootThunkAction = ThunkAction<void, RootState, any, RootAction>

在store/reducers/index.ts

import { Channel, ChannelAction } from '../actions/channel'

// 提供了channel默认的数据的类型
type ChannelType = {
  channelList: Channel[]
  active: number
}

// 指定初始值的类型
const initValue: ChannelType = {
  channelList: [],
  active: 0
}

// 指定了action的类型和返回值的类型
export default function channel(
  state = initValue,
  action: ChannelAction
): ChannelType {
  if (action.type === 'channel/getChannelList') {
    return {
      ...state,
      channelList: action.payload
    }
  }
  return state
}

频道列表数据的渲染

  • 在components/Channel.ts中通过useSelector获取频道的数据
import { RootState } from '../store'

const channel = useSelector((state: RootState) => state.channel)
  • 渲染频道列表
<ul className="catagtory">
  {channel.channelList.map((item) => {
    return (
      <li
        key={item.id}
        className={channel.active === item.id ? 'select' : ''}
      >
        {item.name}
      </li>
    )
  })}
</ul>

处理频道高亮

在store/actions/channel.ts

export type ChannelAction =
  | {
      type: 'channel/getChannelList'
      payload: Channel[]
    }
  | {
      type: 'channel/changeActive'
      payload: number
    }

export function changeActive(id: number): RootThunkAction {
  return (dispatch) => {
    dispatch({
      type: 'channel/changeActive',
      payload: id
    })
  }
}

在store/reducers/channel.ts


export default function channel(
  state = initValue,
  action: ChannelAction
): ChannelType {
  if (action.type === 'channel/getChannelList') {
    return {
      ...state,
      channelList: action.payload
    }
  }
  if (action.type === 'channel/changeActive') {
    return {
      ...state,
      active: action.payload
    }
  }
  return state
}

在components/Channel.tsx组件中注册事件

<li
  key={item.id}
  className={channel.active === item.id ? 'select' : ''}
  onClick={() => dispatch(changeActive(item.id))}
>

文章列表数据的获取

在store/actions/article.ts

import axios from 'axios'
import { RootThunkAction } from '..'

export function getArticleList(id: number): RootThunkAction {
  return async (dispatch) => {
    const res = await axios.get(
      `http://geek.itheima.net/v1_0/articles?channel_id=${id}&timestamp=${Date.now()}`
    )
    console.log(res)
  }
}

在组件中components/NewsList.tsx中

export default function NewsList() {
  const active = useSelector((state: RootState) => state.channel.active)
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(getArticleList(active))
  }, [dispatch, active])

文章列表数据的渲染

在store/actions/article.ts

import axios from 'axios'
import { RootThunkAction } from '..'

export type Article = {
  art_id: string
  title: string
  aut_id: string
  comm_count: number
  pubdate: string
  aut_name: string
  is_top: number
  cover: {
    type: number
    images: string[]
  }
}
export type ArticleAction = {
  type: 'article/getArticleList'
  payload: Article[]
}

export function getArticleList(id: number): RootThunkAction {
  return async (dispatch) => {
    const res = await axios.get(
      `http://geek.itheima.net/v1_0/articles?channel_id=${id}&timestamp=${Date.now()}`
    )
    dispatch({
      type: 'article/getArticleList',
      payload: res.data.data.results
    })
  }
}

在store/reducers/article.ts中

import { Article, ArticleAction } from '../actions/article'

type AritcleType = Article[]
const initValue: AritcleType = []
export default function article(
  state = initValue,
  action: ArticleAction
): AritcleType {
  if (action.type === 'article/getArticleList') {
    return action.payload
  }
  return state
}

在组件中渲染

import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import avatar from '../assets/back.jpg'
import { RootState } from '../store'
import { getArticleList } from '../store/actions/article'
export default function NewsList() {
  const active = useSelector((state: RootState) => state.channel.active)
  const dispatch = useDispatch()
  const articleList = useSelector((state: RootState) => state.article)
  useEffect(() => {
    dispatch(getArticleList(active))
  }, [dispatch, active])
  return (
    <div className="list">
      {articleList.map((item) => {
        return (
          <div className="article_item" key={item.art_id}>
            <h3 className="van-ellipsis">{item.title}</h3>
            <div className="img_box">
              <img src={avatar} className="w100" alt="" />
            </div>
            <div className="info_box">
              <span>13552285417</span>
              <span>0评论</span>
              <span>2018-11-29T17:02:09</span>
            </div>
          </div>
        )
      })}
    </div>
  )
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值