類型即正義: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 倉庫加星❤️哦~

相關文章
相關標籤/搜索