Redux Typescript 大型软件实践

Intro

大多数程序员都将类型安全理解为编程语言的一个特性,TypeScript 作为 JavaScript 的超集,它可以更严格地执行类型检查。

我们拿一个螃蟹 🦀️ 举例,我们知道它只能向左走或向右走,于是我们定义它的方向:

type Direction = "left" | "right";

然后定义它的类:

class Crab {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  move(direction: Direction, distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters} m.`);
  }
}

然后我们去了夏威夷,抓了一只小螃蟹:

const crab = new Crab("a just catched crab from Hawaii");

让它向前走:

crab.move("forward", 20);

于是出错了!

error TS2345: Argument of type '"forward"' is not assignable to parameter of type 'Direction'.

14 crab.move("forward", 20);
             ~~~~~~~~~

这个出错是什么意思呢?它告诉我们 forward 不可以赋值给类型是 Direction 的参数,到这里我们知道类型检查开始工作了。

forward

我们只能让我们的小螃蟹向左或向右走:

crab.move("left", 20);
crab.move("right", 20);

针对 Direction 类型,我们还可以用模式匹配来进行类型检查。

假设我们的左岸是大海,右岸是陆地,而我们的螃蟹智商极高,当它移动时,它知道食物在海里,从而远离陆地。

vscode 会提示我们,只能向左走向右走,其他方向时不允许的

forward

当我们谈到 pattern matching 时,是真的要讲 一只螃蟹向左走向右走 的问题吗?

Either Type

为什么要使用 Either Type?

Either Type 算是一种容器,它可以存储两种类型:A 和 B,这里是 Left 和 Right,一种表示失败,一种表示成功。

学过 Rust 的人都知道,Rust 有个 Result 类型:

#![allow(unused)]
fn main() {
pub enum Result<T, E> {
    /// Contains the success value
    Ok(T),

    /// Contains the error value
    Err(E),
}
}

它在 pattern match 的时候很有用,出错处理几乎离不开它:

#![allow(unused)]
fn main() {
fn halves_if_even(i: i32) -> Result<i32, Error> {
    if i % 2 == 0 { Ok(i/2) } else { Err(/* something */) }
}

fn do_the_thing(i: i32) -> Result<i32, Error> {
    let i = match halves_if_even(i) {
        Ok(i) => i,
        e => return e,
    };

    // use `i`
}
}

我们可以效仿 Rust 实现 TypeScript 的 match on Either

我们使用 union type 来定义 Either

type Left<T> = { type: "left"; value: T };
type Right<T> = { type: "right"; value: T };
type Either<L, R> = Left<L> | Right<R>;

Either 定义了一个容器,实际编码中,我们需要从 Either 容器里提取结果,为了调用者的方便,我们允许传入 callback 来处理不同的情况

function match<T, L, R>(
  input: Either<L, R>,
  left: (left: L) => T,
  right: (right: R) => T
) {
  switch (input.type) {
    case "left":
      return left(input.value);
    case "right":
      return right(input.value);
  }
}

调用者此时可以定义自己的函数,返回类型是个 Either,失败返回 Error,成功得到运动的方向

function validateCrabMoveDirection(
  crab: Crab
): Either<Error, { direction: Direction }> {
  if (crab.name === "strange crab") {
    // return Left type
    return { type: "left", value: Error("x") };
  }
  // return Right type
  return { type: "right", value: { direction: crab.smartmove("right") } };
}

于是可以用 match 来获取上述函数的运行结果:

{
  const direction = match(
    validateCrabMoveDirection(crab),
    (_) => null,
    (right) => right.direction
  );
  // output: right
  console.log(direction);
}

{
  const crab = new Crab("strange crab");
  const direction = match(
    validateCrabMoveDirection(crab),
    (_) => null,
    (right) => right.direction
  );
  // output: null
  console.log(direction);
}

Type-safe action creator in Redux

讲到这里不得不讲下 TypeScriptRedux 中的应用,当我们给 redux 的 action type 定义很多类型时,一个显著的问题是,不同 action creator 的函数类型不能动态获取,此时我们可以利用 TypeScriptReturnType 来解决

以一个 Notes 应用为例

首先定义 Notes 的 interface

interface Note {
  id: number;
  title: string;
  content: string;
  creationDate: string;
}

然后定义 action type,可以用 const

const FETCH_REQUEST = "FETCH_REQUEST";
const FETCH_SUCCESS = "FETCH_SUCCESS";
const FETCH_ERROR = "FETCH_ERROR";

或者使用 enum

const enum NotesActionTypes {
  FETCH_REQUEST = "@@notes/FETCH_REQUEST",
  FETCH_SUCCESS = "@@notes/FETCH_SUCCESS",
  FETCH_ERROR = "@@notes/FETCH_ERROR",
}

然后定义我们的 action creator,此时用到 typesafe-actions 这个库

const fetchRequest = createAction(NotesActionTypes.FETCH_REQUEST);
const fetchSuccess = createAction(NotesActionTypes.FETCH_SUCCESS, (action) => {
  return (data: Note[]) => action(data);
});
const fetchError = createAction(NotesActionTypes.FETCH_ERROR, (action) => {
  return (message: string) => action(message);
});

每个 action creator 的返回类型不同,此时 ReturnType 登场

// 利用 ReturnType 定义 action 减少代码冗余

const actions = { fetchRequest, fetchSuccess, fetchError };
type Action = ReturnType<(typeof actions)[keyof typeof actions]>;

定义了上述 Action,我们就可以给我们的 reducer 中的 action 做类型检查了

// 定义 redux state
type State = { notes: Note[]; state: string; errorMessage?: string };

// 定义 redux reducer
const reducer: Reducer<State> = (state: State, action: Action) => {
  switch (action.type) {
    case getType(fetchRequest): {
      return { ...state, state: "LOADING" };
    }
    case getType(fetchSuccess): {
      return { ...state, state: "LOADED", notes: action.payload };
    }
    case getType(fetchError): {
      return {
        ...state,
        state: "ERROR",
        notes: [],
        errorMessage: action.payload,
      };
    }
    default: {
      return state;
    }
  }
};

简单测试

let state = { notes: [], state: "INIT" };
state = reducer(state, fetchRequest());
// { notes: [], state: 'LOADING' }
console.log(state);

后记

关于为何螃蟹要横向走?来自维基百科

因为腿关节构造的缘故,螃蟹横著走会比较迅速,因此它们一般都是横著行进的,另外,蛙蟹科的一些生物也会直着或倒退着行进。

螃蟹富含优质蛋白质,蟹肉较细腻,肌肉纤维中含有 10 余种游离氨基酸,其中谷氨酸、脯氨酸、精氨酸含量较多,对术后、病后、慢性消耗性疾病等需要补充营养的人大有益处。螃蟹脂肪含量很低,但维生素 A、E 和 B 族较高,特别是蟹黄中富含维生素 A,有益于视力及皮肤健康。蟹富含矿物元素钙、镁以及锌、硒、铜等人体必需的微量元素。但由于螃蟹高胆固醇、高嘌呤,痛风患者食用时应自我节制,患有感冒、肝炎、心血管疾病的人不宜食蟹。死蟹不能吃,会带有大量细菌和毒素。

题图 https://pixabay.com/users/skylark-201564/