类型即正义: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 仓库加星❤️哦~

相关文章
相关标签/搜索