類型即正義:TypeScript 從入門到實踐(三):類型別名和類

咱們研發開源了一款基於 Git 進行技術實戰教程寫做的工具,咱們圖雀社區的全部教程都是用這款工具寫做而成,歡迎 Starcss

若是你想快速瞭解如何使用,歡迎閱讀咱們的 教程文檔前端

學習了註解函數,又瞭解了類型運算如聯合類型和交叉類型,接下來咱們來了解一些 TS 中獨有的類型別名,它相似 JS 變量,是類型變量,接着咱們還會學習 TS 中內容很是龐雜的內容之一:類,瞭解 TS 中類的獨有特性,以及如何註解類,甚至用類去註解其餘內容。react

歡迎閱讀 類型即正義,TypeScript 從入門到實踐系列:git

本文所涉及的源代碼都放在了 Github  或者 Gitee 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+GithubGitee倉庫加星❤️哦~github

此教程屬於 React 前端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~typescript

運行代碼

若是你偏心 碼雲,那麼你能夠運行以下命令獲取這一步的代碼,而後你能夠跟着文章的內容將代碼作出修改:npm

git clone -b part-three https://gitee.com/tuture/typescript-tea.git
cd typescript-tea && npm install && npm start
複製代碼

若是你偏心 Github,那麼你能夠運行以下命令來獲取初始代碼:bash

git clone -b part-thre git@github.com:tuture-dev/typescript-tea.git
cd typescript-tea && npm install && npm start
複製代碼

類型別名

就像咱們爲了在平時開發中更加靈活而建立變量或者幹掉硬編碼數據同樣,TS 爲咱們提供了類型別名,它容許你爲類型建立一個名字,這個名字就是類型的別名,進而你能夠在多處使用這個別名,而且有必要的時候,你能夠更改別名的值(類型),以達到一次替換,多處應用的效果。前端工程師

咱們來看一個簡單的類型別名的例子,假如咱們有一個獲取一我的姓名的函數,它接收一個參數,這個參數有可能直接是要獲取的姓名,它是一個 string 類型,也有多是一個另一個函數,須要調用它以獲取姓名,它是一個函數類型,咱們來看一下這個例子:antd

function getName(n) {
  if (typeof n === 'string') {
    return n;
  } else {
    return n();
  }
}
複製代碼

若是咱們要給這個 n 進行類型註解,那麼它應該同時是 string | () => string ,是 string 類型和 () => string 函數類型的聯合類型,有過必定開發經驗的同窗可能會發覺,這樣寫可能很影響原代碼的可讀性,並且這個 n 的類型可能會變化,由於咱們的函數可能擴展,因此若是咱們用一個類型別名把這個 n 的類型表示出來,那麼就相似咱們用變量替代了硬編碼,可擴展性就更強了,咱們立刻來嘗試一下:

type NameParams = 'string' | () => 'string';

function getName(n: NameParams): string {
  // ... 其它同樣
}
複製代碼

能夠看到咱們用了一個 NameParams 類型別名,它保存着原聯合類型,類型別名就是等號左邊是 type 關鍵字加上別名變量,等號右邊是帶保存的類型,這個類型很廣,它能夠是字面量類型,基礎類型,元組、函數、聯合類型和交叉類型、甚至還能夠是其餘類型別名的組合。

因此對於上面的 NameParams ,咱們能夠進一步拆解它爲以下的樣子:

type Name = string;
type NameResolver = () => string;
type NameParams = Name | NameResolver;

function getName(n: NameParams): Name {
  // ... 其餘同樣
}
複製代碼

咱們看到,上面這個不只更加細粒度,咱們將 NameParams 拆成了兩個類型別名:NameNameResolver ,分別處理 string() => string 的狀況,而後經過聯合操做符聯合賦值給 NameParams ;還帶來了一個優點,咱們的返回值能夠更加明確就是 Name 類型,這樣 Name 變化,它可能變成 number 類型,那麼也能很好的反應這個變化,且只須要修改一下 Name 的值爲 number 類型就能夠了,全部其餘的 Name 類型會自動變化。

類型別名與接口

有同窗讀到這裏,可能有疑問了,這個類型別名貌似無所不能嘛,那它和接口有什麼區別了?

接口主要是用來定義一個結構的類型,好比定義一個對象的類型,而類型別名能夠是任意細粒度的類型定義,好比咱們前面講的最原子的字母量類型如 'hello tuture' 類型,到對象類型如:

type tuture = {
  tutureCommunity: string;
  editure: string;
  tutureDocs: string;
}
複製代碼

上面這個類型咱們定義了一個包含三個屬性的對象類型,並用 tuture 別名來存儲它們。

定義上面這個對象的類型咱們能夠用以前學到的接口這樣寫:

interface Tuture {
  tutureCommunity: string;
  editure: string;
  tutureDocs: string;
}
複製代碼

能夠看到類型別名既能夠表達接口所表達的類型,還比接口更加細粒度,它還能夠是一個基礎類型如 type name = 'string'

動手實踐

還記得以前咱們那個 src/TodoList.tsxAction 組件的 onClick 方法的參數 key 嘛?它是一個聯合類型類型 "complete | delete" ,咱們在多出處用到它,如今咱們是硬編碼寫在了程序裏,將來這個 key 可能會變化,因此咱們須要換成類型別名來表達它們,打開 src/TodoList.tsx ,對其中的內容做出對應的修改以下:

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

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

type MenuKey = "complete" | "delete";

interface ActionProps {
  onClick: (key: MenuKey) => void;
  isCompleted: boolean;
}

// ...

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

function TodoList({ todoList, onClick }: 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={() => (
                  <Action
                    isCompleted={item.isCompleted}
                    onClick={(key: MenuKey) => onClick(item.id, key)}
                  />
                )}
              >
                <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;
複製代碼

能夠看到,咱們定義了一個 MenuKey 類型別名,它表示原聯合類型 complete | delete ,而後咱們替換了組件中三處使用到這個聯合類型的 onClick 函數的參數 key ,將其用 MenuKey 來註解。

其次咱們還刪除了 antd@ant-design/icons 裏面的多餘導出。

繼續改進

接着咱們再來對 TodoList 作一點改變,導出一下咱們剛剛定義的 MenuKey ,由於還有其餘的地方使用到它,咱們打開 src/TodoList.tsxMenuKey 添加 export 前綴,導出咱們的類型別名:

// ...

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

export type MenuKey = "complete" | "delete";

interface ActionProps {
  onClick: (key: MenuKey) => void;
  isCompleted: boolean;
}
 // ...
複製代碼

接着咱們在 src/App.tsx 裏面導入咱們的 MenuKey 類型別名,並替換對應的 onClick 的參數 key 的類型註解爲 MenuKey

import React, { useRef, useState } from "react";
import { Button, Typography, Form, Tabs } from "antd";

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

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

import "./App.css";
import logo from "./logo.svg";
 // ...
function App() {
  const [todoList, setTodoList] = useState(todoListData);

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

  const onClick = (todoId: string, key: MenuKey) => {
    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>
  );
}

export default App;
複製代碼

能夠看到如上文件裏面,咱們還刪除了一些 antd 裏面沒必要要的包導入。

小結

這一節咱們學習了類型別名,它能夠在必定程度上模擬接口(Interface),同時在類型上又能夠達到比接口更加細粒度的效果,同時它又像 JS 中的變量,能夠一處修改,多處生效,避免硬編碼類型帶來的一些代碼上的重構和改動難題。

在進行類的類型註解以前,咱們首先先來了解一下類的組成:

  • 構造函數
  • 屬性
  • 實例屬性
  • 靜態屬性
  • 方法
  • 實例方法
  • 靜態方法

這是 ES6 裏面類的一個組成,那麼在 TS 裏面咱們該如何註解這些內容了?主要有以下組成:

  • 註解構造函數
  • 註解屬性:
  • 訪問限定符: public/protected/private
  • 修飾符:readonly
  • 註解方法
  • 訪問限定符:public/protected/private

簡單註解

瞭解了類大體須要進行類型註解的部分,咱們來具體體驗一下這個註解過程。

首先咱們來看一個動物類:

class Animal {
  name;

  static isAnimal(a) {
    return a instanceof Animal;
  }
  
  constructor(name) {
    this.name = name;
  }

  move(distance) {
    console.log(`Animal moved ${distance}m.`);
  }
}
複製代碼

咱們能夠看到上面這個類的四個部分:

  • 實例屬性 name ,它通常是 string 類型,靜態屬性註解同實例屬性相似
  • 靜態方法 isAnimal ,按照以前講解的註解的函數方式進行註解:1)註解參數 2)註解返回值
  • 構造函數,註解參數
  • 普通方法,按照以前講解的註解的函數方式進行註解:1)註解參數 2)註解返回值

瞭解以後,咱們來註解一下上面這個類:

class Animal {
  name: string;

  static isAnimal(a: Animal): boolean {
    return a instanceof Animal;
  }

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

  move(distance: number) {
    console.log(`Animal moved ${distance}m.`);
  }
}
複製代碼

能夠看到,通過註解後的類看起來也很熟悉,由於都是以前學過的,這裏有個惟一的不一樣就是咱們的靜態方法 isAnimal ,它接收的參數 aAnimal 類自己來註解的,這裏就涉及到兩個知識:

  • 類能夠拿來進行類型註解

  • 類的實例均可以用類名來註解

這兩個知識咱們將在後面講解構造函數時詳細講解。

訪問限定符

除了簡單註解,TS 還給類賦予了一些獨特的內容,其中一個就是大多數靜態語言都有的訪問限定符:publicprotectedprivate ,這些內容讀者可能看起來很陌生了,咱們接下來就來仔細講一講。

Public

public 表明公共的,表示被此訪問限定符修飾的屬性,方法能夠任何地方訪問到:1)類中 2)類的實例對象 3)類的子類中 4)子類的實例對象 等,默認全部類的屬性和方法都是 public 修飾的,好比咱們拿上面那個 Animal 類來舉例:

class Animal {
  public name: string;
  // ...
  public constructor(name: string) { // 函數體 }
  // ...
} 
複製代碼

能夠看到其實咱們的 name 屬性和構造函數等,他們默認都是 public 訪問限定符,這樣咱們能夠在任何地方訪問到這些屬性,下面咱們就來看看如何訪問這些屬性。

在類內部訪問:

class Animal {
  public name: string; 

  public constructor(name: string) { // 函數體 }

  move(distance: number) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

const bird = new Animal('Tuture');
bird.move(520); // 打印 `Tuture moved 520m.`
複製代碼

能夠看到,咱們在類內部的 move 方法內訪問了 public 類型的 name 屬性。

在類外部訪問:

const animal = new Animal('bird');
console.log(animal.name) // 打印 bird
複製代碼

能夠看到,上面咱們經過類 Animal 的實例 animal 訪問到了 name 屬性。

在子類中訪問:

class Bird extends Animal {
  fly() {
    console.log(`${this.name} can fly!`); 
  }
}

const bird = new Bird('Tuture');
bird.fly() // 打印 `Tuture can fly!`
複製代碼

能夠看到,上面咱們在類 Animal 的子類 Bird 內部的 fly 方法訪問到了 name 屬性。

在子類外部訪問:

class Bird extends Animal {
  fly() {
    console.log(`${this.name} can fly!`);
  }
}

const bird = new Bird('Tuture');
console.log(bird.name) // 打印 Tuture
複製代碼

能夠看到,上面咱們在子類 Bird 的實例 bird 上面訪問到了 name 屬性。

Protected

接下來咱們來看一下第二個訪問限定符 protected ,它的字面意思是 「受保護的」,比 public 的可訪問的範圍要小一些,它只能在類和子類中訪問,不能被類的實例對象訪問也不能被子類的實例對象訪問,也就是上面 public 的三種訪問裏面,被 protected 訪問限定符修飾的只能在第一類和第三類裏面被訪問到:

在類中訪問:

class Animal {
  protected name: string;

  public constructor(name: string) { // 函數體 }

  move(distance: number) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

const bird = new Animal('Tuture');
bird.move(520); // 打印 `Tuture moved 520m.`
複製代碼

能夠看到,咱們在類內部的 move 方法內訪問了 public 類型的 name 屬性。

在子類中訪問:

class Animal {
  protected name: string;
  constructor(name: string) {
    this.name = name
  }
}

class Bird extends Animal {
  fly() {
    console.log(`${this.name} can fly!`);
  }
}

const bird = new Bird('Tuture');
bird.fly() // 打印 `Tuture can fly!`
複製代碼

能夠看到,上面咱們在類 Animal 的子類 Bird 內部的 fly 方法訪問到了 name 屬性。

Private

第三類訪問限定符是 private ,它的字面意思是 「私有的」,也就是說它的能夠訪問訪問是最小的,只能在類的內部訪問到,其餘地方都沒法訪問:

在類中訪問:

class Animal {
  private name: string;
  public constructor(name: string) { // 函數體 }

  move(distance: number) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

const bird = new Animal('Tuture');
bird.move(520); // 打印 `Tuture moved 520m.`
複製代碼

能夠看到,咱們在類內部的 move 方法內訪問了 public 類型的 name 屬性。

只讀修飾符

就像咱們以前學習的接口(Interface )時能夠用 readonly 修飾接口的屬性同樣,咱們也能夠用 readonly 修飾類的屬性,好比咱們動物的簡介一旦肯定就不會變了,咱們能夠這樣來寫:

class Animal {
  readonly brief: string = '動物是多細胞真核生命體中的一大類羣,可是不一樣於微生物。';
  // ...其餘同樣
}
複製代碼

除了屬性,咱們還能夠用 readonly 來修飾類中方法的參數,好比咱們在設置此動物的類型時,通常能夠給一個默認的類型:

class Animal {
  type: string;
  
  setType(type: string, readonly defaultType = '哺乳動物') {
    this.type = type || defaultType;
  }
}
複製代碼

抽象類

抽象類與抽象方法

TS 另一個特性就是抽象類,什麼是抽象類了?咱們來看個例子:

abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log("Roaming the earth...");
  }
}
複製代碼

能夠看到抽象類就是在類以前加上 abstract 關鍵字,同時,它還不容許被實例化,也就是說以下的操做是不容許的:

const bird = new Animal() // Error
複製代碼

除此以外,抽象類相比普通類還有一個額外的特性就是,能夠在抽象類中定義抽象方法,就像咱們上面的 makeSound 方法,在普通的方法定義以前加上 abstract 關鍵字,這個抽象方法相似於接口裏面的方法的類型定義:1)註解參數和返回值 2)不給出具體的實現,如上面的 move 就是存在具體的實現,而 makeSound 不給出具體的實現。

抽象類的繼承

抽象類只能夠被繼承,不能夠被實例化,且抽象類的繼承與普通類也存在不一樣,普通類的繼承能夠只是簡單的繼承,並不須要額外的操做:

class Animal {
  // Animal 相關的屬性
}

class Bird extends Animal {
  // 不須要作任何操做
}
複製代碼

可是若是一個類繼承另一個抽象類,那麼它必須得實現抽象類中的抽象方法:

abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log("Roaming the earth...");
  }
}

class Bird extends Animal {
  makeSound(): void {
    console.log('Tuture tuture tuture.');
  }
}
複製代碼

能夠看到,上面咱們定義了一個 Bird 類,它繼承自 Animal 抽象類,它必須得實現 makeSound 抽象方法。

構造函數

經過上面的講解咱們基本瞭解了 TS 中的類相比 JS 額外增長的特性,主要是講解了如何註解類的相關部份內容,接下來咱們着重來談一談如何用類來註解其餘內容。這裏爲何類能夠做爲類型來註解其餘內容了?原來在 TS 中聲明一個類的同時會建立多個聲明:

1)第一個聲明是一個類型,這個類型是這個類實例對象類型,用於註解類的實例對象。

2)第二個聲明則是類的構造函數,咱們在實例化類時,就是經過 new 關鍵字加上這個構造函數調用來生成一個類的實例。

聲明註解類實例的類型

可能上面的概念聽得有點懵,咱們拿以前那個例子來實際演示一下。

class Animal {
  name: string;

  static isAnimal(a: Animal): boolean {
    return a instanceof Animal;
  }

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

  move(distance: number) {
    console.log(`Animal moved ${distance}m.`);
  }
}

const bird: Animal = new Animal('Tuture');
複製代碼

這第一個聲明的用於註解類實例對象的類型就是咱們上面的 Animal ,當咱們聲明瞭一個 Animal 類以後,咱們能夠用這個 Animal 來註解 Animal 的實例如 bird 或者 isAnimal 方法中的 a 參數,當你理解了這個概念以後,你會發現 isAnimal 方法只容許傳入爲 Animal 實例的參數 a ,而後返回一個 a instance Animal 的布爾值,這是一個永遠返回 true 的函數。

提示

這裏這個聲明的 Animal 類型不包括構造函數 constructor 以及類中的靜態方法和靜態屬性,就像實例對象中是不包含類的構造函數、靜態方法和靜態屬性同樣。

聲明構造函數

瞭解了第一個聲明,那麼第二個聲明又是什麼意思了?其實就是上面咱們執行 new Animal('Tuture') 來生成一個實例時,這裏的 Animal 實際上就是一個構造函數,經過 new Animal('Tuture') 調用實際上就是調用咱們類裏面的 constructor 函數。

那麼有的同窗看到這裏就有疑問了,咱們的 Animal 類型是用來註解類的實例的,那麼類的構造函數 Animal 該如何註解了?咱們來看這樣一個例子:

let AnimalCreator = Animal;
複製代碼

在這段代碼中,咱們將 Animal 構造函數賦值給 AnimalCreator ,那麼咱們如何註解這個 AnimalCreator 變量的類型了?固然 TS 具備自動類型推導機制,通常狀況下咱們是不須要註解這個變量的,但這裏若是咱們要註解它,那麼該如何註解了?答案是能夠藉助 JS 原有的 typeof 方法:

let AnimalCreator: typeof Animal = Animal;
複製代碼

咱們經過 typeof Animal 獲取構造函數 Animal 的類型,而後用此類型註解 AnimalCreator

類與接口

上面咱們瞭解了類在聲明的時候會聲明一個類型,此類型能夠用於註解類的實例,其實這個類型和咱們以前學習的接口(Interface )有殊途同歸之妙,具體類與接口結合使用的時候有以下場景:

  • 類實現接口

  • 接口繼承類

  • 類做爲接口使用

類實現接口

類通常只能繼承類,可是多個不一樣的類若是共有一些屬性或者方法時,就能夠用接口來定義這些屬性或者方法,而後多個類來繼承這個接口,以達到屬性和方法複用的目的,好比咱們有兩個類 Door (門)和 Car (車),他們都有 Alarm (報警器)的功能,可是他們又是不一樣的類,這個時候咱們就能夠定義一個 Alarm 接口:

interface Alarm {
  alert(): void;
}

class Car implements Alarm {
  alert() {
    console.log('Car alarm');
  }
}
class Door implements Alarm {
  alert() {
    console.log('Door alarm');
  }
}
複製代碼

此時的接口 Alarm 和咱們以前定義的抽象類相似,接口中的方法 alert 相似抽象類中的抽象方法,一旦類實現 (implements )了這個接口,那麼也要實現這個接口中的方法,好比這裏的 alert

和類的單繼承不同,一個類能夠實現多個接口,好比咱們的車還能夠開燈,那麼咱們能夠定義一個 Light 接口,給車整上燈:

interface Alarm {
  alert(): void;
}

interface Light {
  lightOn(): void;
  lightOff(): void;
}

class Car implements Alarm, Light {
  alert() {
    console.log('Car alarm');
  }

  lightOn() {
    console.log('Car lighton');
  }

  lightOff() {
    console.log('Car lightoff');
  }
}
複製代碼

接口繼承類

接口之因此能夠繼承類是由於咱們以前說到了類在聲明的時候會聲明一個類型,此類型用於註解類的實例。而接口繼承類就是繼承這個聲明的類型,咱們來看一個例子:

class Point {
  x: number;
  y: number;
}

interface Point3d extends Point {
  z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };
複製代碼

能夠看到,接口 Point3d 繼承自類 Point ,獲取了來自類的 xy 屬性,實際上接口繼承的是聲明 Point 類時同時聲明的用於註解類實例的那個類型,而這個類型只包含類的實例屬性和方法,因此接口繼承類也是繼承此類的實例屬性和方法的類型。

類做爲接口使用

類做爲接口使用的場景主要在咱們給 React 組件的 PropsState 進行類型註解的時候,咱們既要給組件的 Props 進行類型註解,有時候還要設置組件的 defaultProps 值,這裏的 Props 的註解和 defaultProps 值設置本來須要分開進行,咱們來看一個例子:

interface TodoInputProps {
  value: string;
  onChange: (value: string) => void;
}

interface TodoInputState {
  content: string;
  user: string;
  date: string;
}

const hardCodeDefaultProps = {
  value: 'tuture',
  onChange(value: string) { console.log(`Hello ${value}`); }
}

class TodoInput extends React.Component<TodoInputProps, TodoInputState> {
  static defaultProps: TodoInputProps = hardCodeDefaultProps;

  render() {
    return <div>Hello World</div>;
  }
}
複製代碼

能夠看到,上面是一個標準的 React 類組件,咱們經過 React.Component<TodoInputProps, TodoInputState> 的形式註解了這個類組件的 PropsState ,經過聲明瞭兩個接口來進行註解,這裏 React.Component<TodoInputProps, TodoInputState> 就是泛型,如今不懂不要緊,咱們將在下一節講解泛型,這裏能夠理解泛型相似 JS 函數,這裏的 <> 相似函數的 () ,而後能夠接收參數,這裏咱們傳入了兩個參數分別註解類的 PropsState

咱們還注意到,咱們聲明瞭這個類的 defaultProps ,而後定義了一個 hardCodeDefaultProps 來初始化這個 defaultProps

這就是常見的 React 類組件的類型註解和默認參數初始化的場景,可是當咱們學了類以後,咱們能夠簡化一下上面的類組件的類型註解和默認參數初始化的操做:

class TodoInputProps {
  value: string = 'tuture';
  onChange(value: string) {
    console.log('Hello Tuture');
  }
}

interface TodoInputState {
  content: string;
  user: string;
  date: string;
}

class TodoInput extends React.Component<TodoInputProps, TodoInputState> {
  static defaultProps: TodoInputProps = new Props();

  render() {
    return <div>Hello World</div>;
  }
}
複製代碼

能夠看到,上面咱們將接口 Props 換成了類 TodoInputProps ,這帶來了一些改變,就是類裏面能夠給出屬性和方法的具體實現,而咱們又知道聲明類 TodoInputProps 的時候會同時聲明一個類型 TodoInputProps ,咱們用這個類型來註解組件的 Props ,而後註解 defaultProps ,而後咱們用聲明類時聲明的第二個內容:TodoInputProps 構造函數來建立一個 TodoInputProps 類型的實例對象並賦值給 defaultProps ,細心的同窗能夠把這段代碼複製到咱們以前的 src/TodoInput.tsx 文件裏,編輯器應該會顯示正常,咱們成功利用了類的特性來幫助咱們的 React 組件簡化代碼,提升了代碼的邏輯性。

動手實踐

學習了類的內容以後,咱們立刻將學到的知識運用在咱們的待辦事項小應用裏面,打開 src/TodoInput.tsx ,對其中的內容做出對應的修改以下:

import React from "react";
import { Input, Select, DatePicker } from "antd";
import { Moment } from "moment";

// ...

interface TodoInputProps {
  value?: TodoValue;
  onChange?: (value: TodoValue) => void;
}

interface TodoInputState {
  content: string;
  user: UserId;
  date: string;
}

class TodoInput extends React.Component<TodoInputProps, TodoInputState> {
  state = {
    content: "",
    user: UserId.tuture,
    date: ""
  };

  private triggerChange = (changedValue: TodoValue) => {
    const { content, user, date } = this.state;
    const { value, onChange } = this.props;

    if (onChange) {
      onChange({ content, user, date, ...value, ...changedValue });
    }
  };

  private onContentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value = {} } = this.props;

    if (!("content" in value!)) {
      console.log("hello");
      this.setState({
        content: e.target.value
      });
    }

    this.triggerChange({ content: e.target.value });
  };

  private onUserChange = (selectValue: UserId) => {
    const { value = {} } = this.props;

    if (!("user" in value!)) {
      this.setState({
        user: selectValue
      });
    }

    this.triggerChange({ user: selectValue });
  };

  private onDateOk = (date: Moment) => {
    const { value = {} } = this.props;
    if (!("date" in value!)) {
      this.setState({
        date: date.format("YYYY-MM-DD HH:mm")
      });
    }

    this.triggerChange({ date: date.format("YYYY-MM-DD HH:mm") });
  };

  public render() {
    const { value } = this.props;
    const { content, user } = this.state;
    return (
      <div className="todoInput">
        <Input
          type="text"
          placeholder="輸入待辦事項內容"
          value={value?.content || content}
          onChange={this.onContentChange}
        />
        <Select
          style={{ width: 80 }}
          size="small"
          defaultValue={UserId.tuture}
          value={value?.user || user}
          onChange={this.onUserChange}
        >
          {userList.map(user => (
            <Option value={user.id}>{user.name}</Option>
          ))}
        </Select>
        <DatePicker
          showTime
          size="small"
          onOk={this.onDateOk}
          style={{ marginLeft: "16px", marginRight: "16px" }}
        />
      </div>
    );
  }
}

export default TodoInput;
複製代碼

能夠看到上面的改動主要有以下幾處:

  • 咱們將以前的函數式組件改爲了類組件,而後定義了一個 TodoInputState 接口,加上以前的 TodoInputProps ,一塊兒以泛型的形式註解類的 PropsState ,接着咱們在類中加上實例屬性 state
  • 接着咱們將 triggerChangeonContentChangeonUserChangeonDateOk 四個方法改爲了類的私有方法。
  • 最後咱們加上了類組件獨有的 render 方法,它是一個 public 類型的方法。

提示

這裏咱們在改造 onContentChange 的時候,用 React.ChangeEvent<HTMLInputElement> 的方式註解了方法參數的 e ,這裏也是泛型的一部分,咱們將在下一節着重講解,這裏能夠理解爲一個 HTMLInputElement類型的的 React.ChangeEvent

那麼有同窗會有疑問了,這裏咱們是如何知道該這樣註解了?實際上,咱們看到 render 方法裏的 Input 組件的 onChange 方法,當咱們把鼠標放上去的時候,編輯器會給出以下提示:

能夠看到,編輯器直接提醒咱們該怎麼註解 event 參數了,果真優秀的編輯器能夠提升生產力啊!

小結

在這一節中,咱們學習了 TS 的類,主要學習了以下知識:

  • 瞭解一個類有哪些組成部分,以及如何註解這些組成部分
  • 瞭解了 TS 類獨有的訪問限定符:publicprotectedprivate
  • 瞭解了 TS 類就像接口同樣,它的屬性或者方法的參數也能夠用 readonly 來修飾
  • 學習了 TS 的抽象類,知道了抽象類的抽象方法以及抽象類不能夠直接被實例化,只能夠被子類繼承,且繼承自抽象類的子類須要實現抽象類的抽象方法,即給出具體的同名方法的方法體
  • 接着,咱們學習了 TS 類的獨特性,同時聲明瞭兩個內容 1)一個用於註解類實例的類型 2)一個用於生成類實例的構造方法
  • 最後,咱們學習了類和接口的一些互相操做的場景 1)多個類實現同一個接口來複用接口的屬性或者方法 2)一個類實現多個接口 3)接口也能夠繼承類,只不過是繼承類聲明時同時聲明的同名類型 4)類做爲接口使用,經過進一步應用類聲明的兩個內容來簡化 React 組件代碼,提升代碼的邏輯性和可複用性。

在這一節最後,咱們稍微引伸了一下泛型,說它相似 JS 裏面的函數,能夠接收類型參數,在下一節中,咱們將重點講解泛型的知識和應用,敬請期待!

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

本文所涉及的源代碼都放在了 Github  或者 Gitee 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊GithubGitee 倉庫加星❤️哦~

相關文章
相關標籤/搜索