类型即正义:TypeScript 从入门到实践(二)

3,716 阅读23分钟

作者:一只图雀
仓库:GithubGitee
图雀社区主站(首发):图雀社区
博客:掘金知乎慕课
公众号:图雀社区
联系我:关注公众号后可以加图雀酱微信哦
原创不易,❤️点赞+评论+收藏 ❤️三连,鼓励作者写出更好的教程。

欢迎阅读 类型即正义,TypeScript 从入门到实践系列:

了解了基础的 TS 类型,接口之后,我们开始了解如何给更加复杂的结构注解类型,这就是我们这节里面要引出的函数,进而我们讲解如何对类型进行运算:交叉类型和联合类型,最后我们讲解了最原子类型:字面量类型,以及如何与联合类型搭配实现类型守卫效果。

本文所涉及的源代码都放在了 Github  或者 Gitee 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+GithubGitee仓库加星❤️哦~

此教程属于 React 前端工程师学习路线的一部分,欢迎来 Star 一波,鼓励我们继续创作出更好的教程,持续更新中~

函数

我们在之前 TodoInputProps 中对 onChange 函数做了类型注解,当时我们没有详细讲解,在这一节中我们就来详细讲解一下 TS 中的函数。

注解函数

比如我们有如下的函数:

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

那么我们该如何注解这个函数了?实际上函数主要的部分就是输入和输出,所以我们在注解函数的时候只需要注解函数的参数和返回值就可以了,因为上述的函数体内是是执行 x+y 操作,以我们的 xy 应该都是 number 数字类型,返回值也是 number 数字类型,所以我们对上面的函数进行类型注解如下:

function add(x: number, y: number): number {
  return x + y;
}

可以看到我们用冒号注解形式给 xy 注解了 number 类型,而对于返回值,我们直接以 add(): number 的形式注解返回值。有时候返回值也可以不写,TS 可以根据参数类型和函数体计算返回值类型,也就是俗称的自动推断类型机制。

函数类型

除了注解函数,有时候我们还涉及到将函数赋值给一个变量,比如如下的例子:

const add = function (x, y) {
  return x + y;
}

这个时候我们一般来注解 add 时候,就需要使用函数类型来注解它,一个函数类型是形如:(args1: type1, args2: type2, ..., args2: typen) => returnType 的类型,所以对于上述的例子我们可以对其注解如下:

const add: (x: number, y: number): number =  function(x, y) {
  return x + y;
}

可能有同学有疑问了,这里我们给 add 变量注解了函数类型,但是我们没有给后面的那个函数进行一个注解啊?其实 TS 会进行类型的自动推导,根据函数类型的结构对比后面的函数,会自动推断出后面函数的 xy 和返回值都为 number

可选参数

就像我们之前接口(Interface)中有可选属性一样,我们的函数中也存在可选参数,因为使用 TS 最大的好处之一就是尽可能的明确函数、接口等类型定义,方便其他团队成员很清晰了解代码的接口,大大提高团队协作的效率,所以如果一个函数可能存在一些参数,但是我们并不是每次都需要传递这些参数,那么它们就属于可选参数的范围。

我们来看一下可选参数的例子,比如我们想写一个构造一个人姓名的函数,包含 firstNamelastName ,但是有时候我们不知道 lastName ,那么这样一个函数该怎么写了?:

function buildName(firstName: string, lastName?: string) {
  // ...
}

可以看到上面我们构建一个人姓名的函数,必须得传递 firstName 属性,但是因为 lastName 可能有时候并不能获取到,所以把它设置为可选参数,所以以下几种函数调用方式都是可以的:

buildName('Tom', 'Huang');
buildName('mRcfps');

重载

重载(Overloads)是 TS 独有的概念,在 JS 中没有,它主要为函数多返回类型服务,具体来说就是一个函数可能会在内部执行一个条件语句,根据不同的条件返回不同的值,这些值可能是不同类型的,那么这个时候我们该怎么来给返回值注解类型了?

答案就是使用重载,通过定义一系列同样函数名,不同参数列表和返回值的函数来注解多类型返回值函数,我们来看一个多类型返回的函数:

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x): any {
  // 如果 x 是 `object` 类型,那么我们返回 pickCard 从 myDeck 里面取出 pickCard1 数据
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // 如果 x 是 `number` 类型,那么直接返回一个可以取数据的 pickCard2
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

let myDeck = [
  { suit: "diamonds", card: 2 },
  { suit: "spades", card: 10 },
  { suit: "hearts", card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

针对上面的这个例子,我们这个 pickCard 函数根据 x 的类型会有不同的返回类型,有的同学可能会有疑问了,之前我们不是说过,TS 能够根据参数类型和函数体自动推断返回值类型嘛?是的,之前那个例子参数类型只有一种选项,所以可以自动推断出返回值类型,但是这里的情况是:“参数类型可能有多种选项,对应不同选项的参数类型,会有不同的返回值类型,并且我们对参数类型还未知”。针对这种情况,我们直接解耦这个对应关系,使用重载就可以很好的表达出来:

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x): any {
  // 如果 x 是 `object` 类型,那么我们返回 pickCard 从 myDeck 里面取出 pickCard1 数据
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // 如果 x 是 `number` 类型,那么直接返回一个可以取数据的 pickCard2
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

let myDeck = [
  { suit: "diamonds", card: 2 },
  { suit: "spades", card: 10 },
  { suit: "hearts", card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

我们可以看到这段代码比上面唯一多了的就是两端 function pickCard(x: type1): type2 语句,所以重载实际上就是函数名一样,参数列表和返回值不一样,我们来解析一下上面多出的两个重载:

  • 第一个重载,我们给参数 x 赋值了一个数组类型,数组的项是一个对象,对象包含两个属性 suitcard ,它们的类型分别为 stringnumber ;接着返回值类型为 number 类型,这个对应 x 的类型为 object 时,返回类型为 number 这种情况。
  • 第二个重载,我们给参数 x 赋值了一个 number 类型,然后返回值类型是一个对象,它有两个属性 suitcard ,对应的类型为 stringnumber ;这个对应 x 的类型为 number 返回值类型为 object 类型这种情况。

动手实践

学习了 TS 的函数之后,我们马上来运用在我们的 待办事项 应用里面,首先我们打开 src/utils/data.ts 对其中的数据做一点修改:

export interface Todo {
  id: string;
  user: string;
  date: string;
  content: string;
  isCompleted: boolean;
}

export interface User {
  id: string;
  name: string;
  avatar: string;
}

export function getUserById(userId: string) {
  return userList.filter(user => user.id === userId)[0];
}

export const todoListData: Todo[] = [
  {
    id: "1",
    content: "图雀社区:汇聚精彩的免费实战教程",
    user: "23410977",
    date: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    id: "2",
    content: "图雀社区:汇聚精彩的免费实战教程",
    user: "23410976",
    date: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    id: "3",
    content: "图雀社区:汇聚精彩的免费实战教程",
    user: "58352313",
    date: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    id: "4",
    content: "图雀社区:汇聚精彩的免费实战教程",
    user: "25455350",
    date: "2020年3月2日 19:34",
    isCompleted: false
  },
  {
    id: "5",
    content: "图雀社区:汇聚精彩的免费实战教程",
    user: "12345678",
    date: "2020年3月2日 19:34",
    isCompleted: true
  }
];

export const userList: User[] = [
  // ...
  {
    id: "23410976",
    name: "pftom",
    avatar: "https://avatars1.githubusercontent.com/u/26423749?s=88&v=4"
  },
  // ...
  {
    id: "12345678",
    name: "pony",
    avatar: "https://avatars3.githubusercontent.com/u/25010151?s=96&v=4"
  }
];

可以看到,上面我们主要做出了如下几处修改:

  • todoListData 的每个元素的 user 字段改为对应 userList 元素的 id ,方便基于 userid 进行用户信息的查找。
  • 接着我们给 todoListData 每个元素添加了 id 方便标志,然后把 time 属性替换成了 date 属性。
  • 接着我们定义了一个 getUserById 函数,用于每个 todo 中根据 user 字段来获取对应的用户详情,包括名字和头像等,这里我们有些同学可能有疑问了,我们给参数做了类型注解,为啥不需要注解返回值了?其实这也是 TS 自动类型推断的一个应用场景,TS 编译器会根据参数的类型然后自动计算返回值类型,所以我们就不需要明确的指定返回值啦。
  • 最后我们导出了 TodoUser 接口。

接着我们类似单独创建 src/TodoInput.tsx 组件给 src/App.tsx 减负一样,尝试创建 src/TodoList.tsx 组件,然后把对应 src/App.tsx 的对应逻辑移动到这个组件里:

import React from "react";
import { List, Avatar, Menu, Dropdown } from "antd";
import { DownOutlined } from "@ant-design/icons";

import { Todo, getUserById } from "./utils/data";

const menu = (
  <Menu>
    <Menu.Item>完成</Menu.Item>
    <Menu.Item>删除</Menu.Item>
  </Menu>
);

interface TodoListProps {
  todoList: Todo[];
}

function TodoList({ todoList }: TodoListProps) {
  return (
    <List
      className="demo-loadmore-list"
      itemLayout="horizontal"
      dataSource={todoList}
      renderItem={item => {
        const user = getUserById(item.user);

        return (
          <List.Item
            key={item.id}
            actions={[
              <Dropdown overlay={menu}>
                <a key="list-loadmore-more">
                  操作 <DownOutlined />
                </a>
              </Dropdown>
            ]}
          >
            <List.Item.Meta
              avatar={<Avatar src={user.avatar} />}
              title={<a href="https://ant.design">{user.name}</a>}
              description={item.date}
            />
            <div
              style={{
                textDecoration: item.isCompleted ? "line-through" : "none"
              }}
            >
              {item.content}
            </div>
          </List.Item>
        );
      }}
    />
  );
}

export default TodoList;

可以看到,上面我们主要做了如下改动:

  • 我们首先导入了 Todo 接口,给 TodoList 组件增加了 TodoListProps 接口用于给这个组件的 props 做类型注解。
  • 接着我们导入了和 getUserById ,用于在 renderItem 里面根据 item.user 获取用户详情信息,然后展示头像和姓名。
  • 接着我们将 item.time 更新为 item.date
  • 最后我们根据待办事项是否已经完成设置了 line-throughtextDecoration 属性,来标志已经完成的事项。

最后我们来根据上面的改进来修改对应的 src/App.tsx

import React, { useRef, useState } from "react";
import {
  List,
  Avatar,
  // ...
  Dropdown,
  Tabs
} from "antd";

import TodoInput from "./TodoInput";
import TodoList from "./TodoList";

import { todoListData } from "./utils/data";

import "./App.css";
import logo from "./logo.svg";

const { Title } = Typography;
const { TabPane } = Tabs;

function App() {
  const [todoList, setTodoList] = useState(todoListData);

  const callback = () => {};

  const onFinish = (values: any) => {
    const newTodo = { ...values.todo, isCompleted: false };
    setTodoList(todoList.concat(newTodo));
  };
  const ref = useRef(null);

  const activeTodoList = todoList.filter(todo => !todo.isCompleted);
  const completedTodoList = todoList.filter(todo => todo.isCompleted);

  return (
    <div className="App" ref={ref}>
      <div className="container header">
        // ...
      <div className="container">
        <Tabs onChange={callback} type="card">
          <TabPane tab="所有" key="1">
            <TodoList todoList={todoList} />
          </TabPane>
          <TabPane tab="进行中" key="2">
            <TodoList todoList={activeTodoList} />
          </TabPane>
          <TabPane tab="已完成" key="3">
            <TodoList todoList={completedTodoList} />
          </TabPane>
        </Tabs>
      </div>
    // ...

可以看到上面的内容作出了如下的修改:

  • 首先我们删除了 TodoList 部分代码,转而导入了 TodoList 组件
  • 接着我们使用 useState Hooks 接收 todoListData 作为默认数据,然后通过 isCompleted 过滤,生成

小结

我们来总结和回顾一下这一小节学到的知识:

  • 首先我们讲解了 TS 中的函数,主要讲解了如何注解函数
  • 然后引出了函数赋值给变量时如何进行变量的函数类型注解,并因此讲解了 TS 具有自动类型推断的能力
  • 接着,我们对标接口(Interface)讲解了函数也存在可选参数
  • 最后我们讲解了 TS 中独有的重载,它主要用来解决函数参数存在多种类型,然后对应参数的不同类型会有不同的返回值类型的情况,那么我们要给这种函数进行类型注解,可以通过重载的方式,解耦参数值类型和返回值类型,将所有可能情况通过重载表现出来。

因为本篇文章是图雀社区一杯茶系列,所以关于函数的知识,我们还有一些内容没有讲解到,不过具体内容都是触类旁通,比如注解函数的 rest 参数,this 等,有兴趣的同学可以查阅官方文档:TS-函数

交叉类型、联合类型

在前三个大章节中,我们我们讲解了基础的 TS 类型,然后接着我们用这些学到的基础类型,去组合形成枚举和接口,去注解函数的参数和返回值,这都是 TS 类型注解到 JS 元素上的实践,那么就像 JS 中有元素运算一样如加减乘除甚至集合运算 “交并补”,TS 中也存在类型的一个运算,这就是我们这一节中要讲解的交叉和联合类型。

交叉类型

交叉类型就是多个类型,通过 & 类型运算符,合并成一个类型,这个类型包含了多个类型中的所有类型成员,我们来看个响应体的例子,假如我们有一个查询艺术家的请求,我们要根据查询的结果 -- 响应体,打印对应信息,一般响应体是两类信息的综合:

  • 请求成功,返回标志请求成功的状态,以及目标数据
  • 请求失败,返回标志请求失败的状态,以及错误信息

针对这一一个场景,我们就可以使用交叉类型,了解了这样一个场景之后,那么我们再来看一下对应这个场景的具体例子:

interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtistsData {
  artists: { name: string }[];
}

const handleArtistsResponse = (response: ArtistsData & ErrorHandling) => {
  if (response.error) {
    console.error(response.error.message);
    return;
  }

  console.log(response.artists);
};

我们可以看到这个例子,我们的艺术家信息接口(Interface)是 ArtistsData ,它是请求成功之后返回的具体数据之一,除了这个,我们的响应体一般还有标志响应是否成功的状态,以及错误的时候的打印信息,所以我们还定义了一个 ErrorHandling ,它们两个进行一个交叉类型操作就组成了我们的艺术家响应体:ArtistsData & ErrorHandling ,然后我们在函数参数里面标志 response 为这个交叉类型的结果,并在函数体之类根据请求是否成功的状态 reponse.error 判断来打印对应的信息。

联合类型

那么联合类型是什么了?联合类型实际上是通过操作符 | ,将多个类型进行联合,组成一个复合类型,当用这个复合类型注解一个变量的时候,这个变量可以取这个复合类型中的任意一个类型,这个有点类似枚举了,就是一个变量可能存在多个类型,但是最终只能取一个类型。

读者这里可以自行了解联合类型和枚举类型的一个细节差异,本文首先于篇幅,不具体展开。

接下来我们来看个联合类型应用的场景,比如我们有一个 padLeft 函数 -- 左填充空格操作,它负责接收两个参数 valuepadding ,主要目标是实现给 value 这个字符串左边添加 padding ,可以类比这个 padding 就是空格,但是这里的 padding 既可以是字符串 string 类型,也可以是数字 number ,当 padding 是字符串时,一个比较简单的例子如下:

const value: string = 'Hello Tuture';
const padding: string = '   ';

padLeft(value, padding) // => '   Hello Tuture';

好的,了解的场景之后,我们马上来一个实战,讲解上面那个例子的一个升级版:

function padLeft(value: string, padding: any) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4);

可以看到这个例子,padding 我们暂时给了 any ,然后函数体里面对 stringnumber 类型给了判断,执行对应的 “左空格填充” 操作,这个逻辑对于研发初期是可行的,但是当我们涉及到多人协作开发的时候,其他成员光看这个函数的变量定义,无法了解到底该给这个 padding 传递一个什么样类型的值,有可能某个队友进行了如下操作:

padLeft('Hello world', true)

啪的一下,这个程序就崩了!所以你看,其实程序还是很脆弱的。

为了更加明确的约束 padding 的类型,我们有必要引进联合类型:

function padLeft(value: string, padding: string | number) {
  // ...中间一样
}

这个时候,我们发现即使再来很多位队友,他们也知道该如何调用这个接口,因为编译器会强制队友写正确的类型,如果还继续写:

padLeft('Hello world', true)

编译器就会提示你如下错误:

小结

这一小节中我们学习了交叉类型和联合类型,它们是 TS 类型系统中的类型运算的产物,交叉类型是多个类型组成一个类型,最终结果类型是多个类型的总和,而联合类型是多个类型组成一个综合体,最终的结果类型是多个类型之中的某一个类型,交叉类型主要用于构造响应体,联合类型主要用于处理单变量被注解为多类型之一的场景,它还会与我们下一节要讲的字面量类型发生化学反应,实现枚举和处理类型守卫,我们将马上来讲解这些神奇的化学反应。

字面量类型与类型守卫

最后我们来聊一聊类型守卫,类型守卫很多场景上都是和联合类型打配合存在的。在讲类型守卫的时候,我们还需要先聊一聊字面量类型,额!其实这三者是相辅相成的。

字面量类型

其实字面量类型我们在第二节中已经或多或少的提到过了,还记得那个报错嘛?

const tutureSlogan: string = 5201314 // 报错 Type '5201314' is not assignable to Type 'string'

这里的 TS 编译器提示,"Type '5201314' is not assignable to Type 'string“,这里的 "Type '5201314'" 实际上就是一个字面量类型。

字面量可是说是 TS 类型系统里面最小的类型,就像 JS 里面的数字 1,它不可能再拆成更小的部分了,一般字面量类型分为两种:

  • 数字字面量
  • 字符串字面量

数字字面量

520 这个数,把它当做类型使用,它就是数组字面量类型,使用它来注解一个变量的时候是这样的:

let tuture: 520

当我们初始化这个 tuture 变量的时候,就只能是赋值 520 这个数字了:

tuture = 520; // 正确
tuture = 521; // 错误 Type '521' is not assignable to type '520'

字符串字面量

对应的字符串字面量类似,我们现在用 '520' 这个字符串字面量类型来注解 tuture

let tuture: '520';

tuture = '520';
tuture = '521'; // Type '"521"' is not assignable to type '"520"'

可以看到字面量类型还带来一个特点就是,被注解的为对应字面量类型的变量,在赋值的时候只能赋值为这个被注解的字面量。

上面我们了解了字面量类型,并且具体谈了谈它们的特点,那么这么一个单纯的类型,到底有什么特别的地方了?其实字面量类型搭配联合类型有意想不到的威力,我们来举两个例子:

  • 实现枚举
  • 实现类型守卫

搭配举例 - 实现枚举效果

当我们搭配联合类型和字面量类型的时候,我们可以实现一定的枚举效果,我们来看个例子,我们买电脑一般都是三种系统,我们可以通过选用这三种电脑类型来获取对应的一个用户的情况,我们现在只给出一个函数的大体框架,具体实现在类型守卫里面详细展开:

function getUserInfo(osType: 'Linux' | 'Mac' | 'Windows') { // ... 后续实现 }

我们可以看到上面的例子,osType 可以取三种操作系统之一的值,这就类似枚举,我们可以创建一个类似的枚举:

enum EnumOSType {
  Linux,
  Mac,
  Windows
}

function getUserInfo(osType: EnumOSType) {}

上面两个例子效果其实差不多,我们就通过 联合类型+字面量类型 实现了一个简单枚举的效果。

类型守卫

类型守卫是我们 联合类型+字面量类型 的又一个应用场景,它主要用于在进行 ”联合“ 的多个类型之间,存在相同的字段,也存在不同的字段,然后需要区分具体什么时候是使用哪个类型,这么说可能比较迷糊,我们来看个例子,加入我们的 getUserInfo 函数的参数接收的是 os ,它根据 os.type 打印对应 os 携带的用户信息:

interface Linux {
  type: 'Linux';
  linuxUserInfo: '极客';
}

interface Mac {
  type: 'Mac';
  macUserInfo: '极客+1';
}

interface Windows {
  type: 'Windows';
  windowsUserInfo: '极客+2';
}

function getUserInfo(os: Linux | Mac | Windows) {
  console.log(os.linuxUserInfo);
}

可以看到上面我们将 osType 扩充成了 os ,然后三种 os 有相同的字段 type 和不同的字段 xxxUserInfo ,但是当我们函数体类打印 os.linuxUserInfo 的时候,TS 编译器报了如下错误:

有同学就有疑问了,我们这里不是联合类型了嘛,那应该 osLinux 这一类型啊,这么打印为什么会错呢?其实我们要抓住一点,联合类型的最终结果是联合的多个类型之一,也就是 os 还可能是 Mac 或者 Windows ,所以这里打印 os.linuxUserInfo 就有问题,所以我们这个时候就需要类型守卫来帮忙了,它主要是根据多个类型中一样的字段,且这个字段是字面量类型来判断,进而执行不同的逻辑来确保类型的执行是正确的,我们来延伸一下上面的那个例子:

function getUserInfo(os: Linux | Mac | Windows) {
  switch (os.type) {
    case 'Linux': {
      console.log(os.linuxUserInfo);
      break;
    }

    case 'Mac': {
      console.log(os.macUserInfo);
      break;
    }

    case 'Windows': {
      console.log(os.windowsUserInfo);
      break;
    }
  }
}

可以看到,如果有同学跟着手敲这个函数的话,会发现当针对 os.type 进行条件判断之后,在 case 语句里面,TS 自动提示了需要取值的类型,比如在 Linux case 语句里面输入 os. 会提示 linux

动手实践

了解完字面量类型和类型守卫之后,我们马上运用在我们的待办事项应用里面。

首先打开 src/TodoList.tsx ,我们近一步完善 TodoList.tsx 的逻辑:

import React from "react";
import { List, Avatar, Menu, Dropdown, Modal } from "antd";
import { DownOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import { ClickParam } from "antd/lib/menu";

import { Todo, getUserById } from "./utils/data";

const { confirm } = Modal;

interface ActionProps {
  onClick: (key: "complete" | "delete") => void;
  isCompleted: boolean;
}

function Action({ onClick, isCompleted }: ActionProps) {
  const handleActionClick = ({ key }: ClickParam) => {
    if (key === "complete") {
      onClick("complete");
    } else if (key === "delete") {
      onClick("delete");
    }
  };

  return (
    <Menu onClick={handleActionClick}>
      <Menu.Item key="complete">{isCompleted ? "重做" : "完成"}</Menu.Item>
      <Menu.Item key="delete">删除</Menu.Item>
    </Menu>
  );
}

interface TodoListProps {
  todoList: Todo[];
  onClick: (todoId: string, key: "complete" | "delete") => void;
}

function TodoList({ todoList, onClick }: TodoListProps) {
  return (
    <List
      className="demo-loadmore-list"
      // ...
          <List.Item
            key={item.id}
            actions={[
              <Dropdown
                overlay={() => (
                  <Action
                    isCompleted={item.isCompleted}
                    onClick={(key: "complete" | "delete") =>
                      onClick(item.id, key)
                    }
                  />
                )}
              >
                <a key="list-loadmore-more">
                  操作 <DownOutlined />
                </a>
              // ...

可以看到上面的改动主要有如下几个部分:

  • 我们扩展了单个 Todo 的点击下拉菜单的菜单组件,定义了一个 Action 组件,它接收两个参数,isCompletedonClick ,前者用来标志现在对 Todo 操作是重做还是完成,后者用来处理点击事件,根据 todo.id 和 操作的类型 key 来处理。
  • 我们在 Action 组件的 onClick 属性里面调用的 onClick 函数是父组件传下来的函数,所以我们需要额外在 TodoListProps 加上这个 onClick 函数的类型定义,按照我们之前学习的注解函数的知识,这里我们需要注解参数列表和返回值,因为 onClick 函数内部执行点击逻辑,不需要返回值,所以我们给它注解了 void 类型,针对参数列表,todoId 比较简单,一般是字符串,所以注解为 string 类型,而 key 标注操作的类型,它是一个字面量联合类型,允许有 completedelete 两种
  • 接着我们来看 Action 组件,我们在上一步已经讲解它接收两个参数,因此我们新增一个 ActionProps 来注解 Action 组件的参数列表,可以看到其中的 onClick 和我们上一步讲解的一样,isCompleted 注解为 boolean
  • 接在在 Action 组件里我们定义了 Menu onClick的处理函数 handleActionClick 是一个ClickParam 类型,它是从 antd/lib/menu 导入的 ,由组件库提供的,然后我们从参数里面解构出来了 key ,进而通过字面量类型进行类型守卫,处理了对于的 onClick 逻辑
  • 最后我们做的一点改进就是在 Menu 里面根据 isCompleted 展示 “重做” 还是 “完成”。

改进了 src/TodoList.tsx ,接着我们再来改进 src/App.tsx 里面对应于 TodoList 的逻辑,我们打开 src/App.tsx 对其中的内容做出对应的修改如下:

import React, { useRef, useState } from "react";
import {
  List,
  Avatar,
  // ...
function App() {
  const [todoList, setTodoList] = useState(todoListData);

  const callback = () => {};
 // ...
  const activeTodoList = todoList.filter(todo => !todo.isCompleted);
  const completedTodoList = todoList.filter(todo => todo.isCompleted);

  const onClick = (todoId: string, key: "complete" | "delete") => {
    if (key === "complete") {
      const newTodoList = todoList.map(todo => {
        if (todo.id === todoId) {
          return { ...todo, isCompleted: !todo.isCompleted };
        }

        return todo;
      });

      setTodoList(newTodoList);
    } else if (key === "delete") {
      const newTodoList = todoList.filter(todo => todo.id !== todoId);
      setTodoList(newTodoList);
    }
  };

  return (
    <div className="App" ref={ref}>
      <div className="container header">
        // ...
      <div className="container">
        <Tabs onChange={callback} type="card">
          <TabPane tab="所有" key="1">
            <TodoList todoList={todoList} onClick={onClick} />
          </TabPane>
          <TabPane tab="进行中" key="2">
            <TodoList todoList={activeTodoList} onClick={onClick} />
          </TabPane>
          <TabPane tab="已完成" key="3">
            <TodoList todoList={completedTodoList} onClick={onClick} />
          </TabPane>
        </Tabs>
      </div>
    </div>
  );
}

export default App;

可以看到上面主要就是两处改动:

  • TodoList 增加 onClick 属性
  • 实现 onClick 函数,根据字面量类型 key 进行类型守卫处理对应的数据更改逻辑

小结

在这个小结中我们学习了字面量类型和类型守卫,字面量类型与联合类型搭配可以实现枚举的效果,也可以处理类型守卫,字面量类型是 TS 中最原子的类型,它不可以再进行拆解,而类型守卫主要是在针对联合类型时,TS 编译器无法处理,需要通过开发者手工辅助 TS 编译器处理类型而存在。

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

本文所涉及的源代码都放在了 Github  或者 Gitee 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞GithubGitee 仓库加星❤️哦~