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

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

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

本文所涉及的源代码都放在了 Github  或者 Gitee 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+GithubGitee仓库加星❤️哦~前端

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

函数

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

注解函数

好比咱们有以下的函数:git

function add(x, y) {
  return x + y;
}
复制代码

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

function add(x: number, y: number): number {
  return x + y;
}
复制代码

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

函数类型

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

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 仓库加星❤️哦~

相关文章
相关标签/搜索