阅读 1740

类型即正义:TypeScript 从入门到实践(三):类型别名和类

我们研发开源了一款基于 Git 进行技术实战教程写作的工具,我们图雀社区的所有教程都是用这款工具写作而成,欢迎 Star

如果你想快速了解如何使用,欢迎阅读我们的 教程文档

学习了注解函数,又了解了类型运算如联合类型和交叉类型,接下来我们来了解一些 TS 中独有的类型别名,它类似 JS 变量,是类型变量,接着我们还会学习 TS 中内容非常庞杂的内容之一:类,了解 TS 中类的独有特性,以及如何注解类,甚至用类去注解其他内容。

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

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

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

运行代码

如果你偏爱 码云,那么你可以运行如下命令获取这一步的代码,然后你可以跟着文章的内容将代码做出修改:

git clone -b part-three https://gitee.com/tuture/typescript-tea.git
cd typescript-tea && npm install && npm start
复制代码

如果你偏爱 Github,那么你可以运行如下命令来获取初始代码:

git clone -b part-thre git@github.com:tuture-dev/typescript-tea.git
cd typescript-tea && npm install && npm start
复制代码

类型别名

就像我们为了在平时开发中更加灵活而创建变量或者干掉硬编码数据一样,TS 为我们提供了类型别名,它允许你为类型创建一个名字,这个名字就是类型的别名,进而你可以在多处使用这个别名,并且有必要的时候,你可以更改别名的值(类型),以达到一次替换,多处应用的效果。

我们来看一个简单的类型别名的例子,假如我们有一个获取一个人姓名的函数,它接收一个参数,这个参数有可能直接是要获取的姓名,它是一个 string 类型,也有可能是一个另外一个函数,需要调用它以获取姓名,它是一个函数类型,我们来看一下这个例子:

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 TodoInputProps();

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