97

你所不知道的 Typescript 与 Redux 类型优化

 6 years ago
source link: https://juejin.im/post/5a3732e5518825127e7454da?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

你所不知道的 Typescript 与 Redux 类型优化

2017年12月18日 03:16 ·  阅读 1473
你所不知道的 Typescript 与 Redux 类型优化

原发于知乎专栏,欢迎关注:zhuanlan.zhihu.com/p/32112508

自从 Redux 诞生后,函数式编程在前端一直很热;去年7月,Typescript 发布 2.0,OOP 数据流框架也开始火热,社区更倾向于类型友好、没有 Redux 那么冗长烦琐的 Mobx 和 dob

然而静态类型并没有绑定 OOP。随着 Redux 社区对 TS 的拥抱以及 TS 自身的发展,TS 对 FP 的表达能力势必也会越来越强。Redux 社区也需要群策群力,为 TS 和 FP 的伟大结合做贡献。

本文主要介绍 Typescript 一些有意思的高级特性;并用这些特性对 Redux 做了类型优化,例如:推导全局的 Redux State 类型、Reducer 每个 case 下拿到不同的 payload 类型;Redux 去形式化与 Typescript 的结合;最后介绍了一些 React 中常用的 Typescript 技巧。

Mapped Types

Javascript 中,字面量对象和数组是非常强大灵活。引进类型后,如何避免因为类型的约束而使字面量对象和数组死气沉沉,Typescript 灵活的 interface 是一个伟大的发明。

下面介绍的 Mapped Types 让 interface 更加强大。大家在 js 中都用过 map 运算。在 TS 中,interface 也能做 map 运算。

// 将每个属性变成可选的。
type Optional<T> = {
 [key in keyof T]?: T[key];
}
复制代码

从字面量对象值推导出 interface 类型,并做 map 运算:

type NumberMap<T> = {
  [key in keyof T]: number;
}

function toNumber<T>(obj: T): NumberMap<T> {
  return Object.keys(obj).reduce((result, key) => {
    return {
      ...result,
      [key]: Number(result[key]),
    };
  }, {}) as any;
}

const obj2 = toNumber({
  a: '32',
  b: '64',
});
复制代码

在 interface map 运算的支持下,obj2 能推导出精准的类型。

获取函数返回值类型

在 TS 中,有些类型是一个类型集,比如 interface,function。TS 能够通过一些方式获取类型集的子类型。比如:

interface Person {
  name: string;
}

// 获取子类型
const personName: Person['name'];
复制代码

然而,对于函数子类型,TS 暂时没有直接的支持。不过江湖上有一种类型推断的方法,可以获取返回值类型。

虽然该方法可以说又绕又不够优雅,但是函数返回值类型的推导,能够更好地支持函数式编程,收益远大于成本。

type Reverse<T> = (arg: any) => T;

function returnResultType<T>(arg: Reverse<T>): T {
  return {} as any as T;
}

// result 类型是 number
const result = returnResultType((arg: any) => 3);
type ResultType = typeof result;
复制代码

举个例子,当我们在写 React-redux connect 的时候,返回结构极有可能与 state 结构不尽相同。而通过推导函数返回类型的方法,可以拿到准确的返回值类型:

type MapProps<NewState> = (state?: GlobalState, ownProps?: any) => NewState;
function returnType<NewState>(mapStateToProps: MapProps<NewState>) {
  return {} as any as NewState;
}
复制代码

使用方法:

function mapStateToProps(state?: GlobalState, ownProp?: any) {
  return {
    ...state.dataSrc,
    a: '',
  };
};

const mockNewState = returnType(mapStateToProps);
type NewState = typeof mockNewState;
复制代码

可辨识联合(Discriminated Unions)

关于 Discriminated Unions ,官方文档已有详细讲解,本文不再赘述。链接如下:

查看英文文档

查看中文文档

可辨识联合是什么,我只引用官方文档代码片段做快速介绍:

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

type Shape = Square | Rectangle;

function area(s: Shape) {
    switch (s.kind) {
        // 在此 case 中,变量 s 的类型为 Square
        case "square": return s.size * s.size;
        // 在此 case 中,变量 s 的类型为 Rectangle
        case "rectangle": return s.height * s.width;
    }
}
复制代码

在不同的 case 下,变量 s 能够拥有不同的类型。我想读者一下子就联想到 Reducer 函数了吧。注意 interface 中定义的 kind 属性的类型,它是一个字符串字面量类型。

redux 类型优化

combineReducer 优化

原来的定义:

type Reducer<S> = (state: S, action: any) => S;

function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
复制代码

粗看这个定义,好似没有问题。但熟悉 Redux 的读者都知道,该定义忽略了 ReducersMapObject 和 S 的逻辑关系,S 的结构是由 ReducersMapObject 的结构决定的。

如下所示,先用 Mapped Types 拿到 ReducersMapObject 的结构,然后用获取函数返回值类型的方法拿到子 State 的类型,最后拼成一个大 State 类型。

type Reducer<S> = (state: S, action: any) => S;

type ReducersMap<FullState> = {
  [key in keyof FullState]: Reducer<FullState[key]>;
}

function combineReducers<FullState>(reducersMap: ReducersMap<FullState>): Reducer<FullState>;
复制代码

使用新的 combineReducers 类型覆盖原先的类型定义后,经过 combineReducers 的层层递归,最终可以通过 RootReducer 推导出 Redux 全局 State 的类型!这样在 Redux Thunk 中和 connect 中,可以享受全局 State 类型,再也不需要害怕写错局部 state 路径了!

拿到全局 State 类型:

function returnType<FullState>(reducersMap: ReducersMap<FullState>): FullState {
  return ({} as any) as FullState;
}

const mockGlobalState = returnType(RootReducer);

type GlobalState = typeof mockGlobalState;
type GetState = () => GlobalState;
复制代码

去形式化 & 类型推导

Redux 社区一直有很多去形式化的工具。但是现在风口不一样了,去形式化多了一项重大任务,做好类型支持!

关于类型和去形式化,由于 Redux ActionCreator 的型别取决于实际项目使用的 Redux 异步中间件。因此本文抛开笔者自身业务场景,只谈方法论,只做最简单的 ActionCreator 解决方案。读者可以用这些方法论创建适合自己项目的类型系统。

经团队同学提醒,为了读者有更好的类型体感,笔者创建了一个 repo 供读者体验:

github.com/jasonHzq/re…

读者可以 clone 下来在 vscode 中进行体验。

Redux Type

enum 来声明 Redux Type ,可以说是最精简的了。

enum BasicTypes {
  changeInputValue,
  toggleDialogVisible,
}

const Types = createTypes(prefix, BasicTypes);
复制代码

然后用 createTypes 函数修正 enum 的类型和值。

createTypes 的定义如下所示,一方面用 Proxy 对属性值进行修正。另一方面用 Mapped Types 对类型进行修正。

type ReturnTypes<EnumTypes> = {
    [key in keyof EnumTypes]: key;
}

function createTypes<EnumTypes>(prefix, enumTypes: EnumTypes): ReturnTypes<EnumTypes> {
    return new Proxy(enumTypes as any, {
        get(target, property: any) {
            return prefix + '/' + property;
        }
    })
}
复制代码

读者请注意,ReturnTypes 中,Redux Type 类型被修正为一个字符串字面量类型(key)!以为创造一个可辨识联合做准备。

Redux Action 类型优化

市面上有很多 Redux 的去形式化工具,因此本文不再赘述 Redux Action 的去形式化,只说 Redux Action 的类型优化。

笔者总结如下3点:

  • 1、要有一个整体 ActionCreators 的 interface 类型。

例如,可以定义定一个字面量对象来存储 actionCreators。

const actions = {
  /** 加 */
  add: ...
  /** 乘以 */
  multiply: ...
}
复制代码

一方面其它模块引用起来会很方便,一方面可以对字面量做批量类型推导。并且其中的注释,只有在这种字面量下,才能够在 vscode 中解析,以在其它模块引用时可以提高辨识度,提高开发体验。

  • 2、每一个 actionCreator 需要定义 payload 类型。

如下代码所示,无论 actionCreator 是如何创建的,其 payload 类型必须明确指定。以便在 Reducer 中享用 payload 类型。

const actions = {
  /** 加 */
  add() {
    return { type: Types.add, payload: 3 };
  },
  /** 乘以 */
  multiply: createAction<{ num: number }>(Types.multiply)
}
复制代码
  • 3、推导出可辨识联合类型。

最后,还要能够通过 actions 推导出可辨识联合类型。如此才能在 Reducer 不同 case 下享用不同的 payload 类型。

需要推导出的 ActionType 结构如下:

type ActionType = { type: 'add', payload: number }
  | { type: 'multiply', payload: { num: number } };
复制代码

推导过程如下:

type ActionCreatorMap<ActionMap> = {
  [key in keyof ActionMap]: (payload?, arg2?, arg3?, arg4?) => ActionMap[key]
};
type ValueOf<ActionMap> = ActionMap[keyof ActionMap];

function returnType<ActionMap>(actions: ActionCreatorMap<ActionMap>) {
  type Action = ValueOf<ActionMap>;

  return {} as any as Action;
}

const mockAction = returnType(actions);
type ActionType = typeof mockAction;

function reducer(state: State, action: ActionType): State {
  switch (action.type) {
    case Types.add: { return ... }
    case Types.muliple: { return ... }
  }
}
复制代码

前端类型优化

常用的React类型

  • Event

React 中 Event 参数很常见,因此 React 提供了丰富的关于 Event 的类型。比如最常用的 React.ChangeEvent:

// HTMLInputElement 为触发 Event 的元素类型
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  // e.target.value
  // e.stopPropagation
}
复制代码

笔者更喜欢把 Event 转换成对应的 value

function pipeEvent<Element = HTMLInputElement>(func: any) {
  return (event: React.ChangeEvent<HTMLInputElement>) => {
    return func(event.target.value, event);
  };
}

<input onChange={pipeEvent(actions.changeValue)}>
复制代码
  • RouteComponentProps

ReactRoute 提供了 RouteComponentProps 类型,提供了 location、params 的类型定义

type Props = OriginProps & RouteComponentProps<Params, {}>
复制代码

自动产生接口类型

一般来说,前后端之间会用一个 API 约定平台或者接口约定文档,来做前后端解耦,比如 rap、 swagger。笔者在团队中做了一个把接口约定转换成 Typescript 类型定义代码的。经过笔者团队的实践,这种工具对开发效率、维护性都有很大的提高。

接口类型定义对开发的帮助:

在可维护性上。例如,一旦接口约定进行更改,API 的类型定义代码会重新生成,Typescript 能够检测到字段的不匹配,前端便能快速修正代码。最重要的是,由于前端代码与接口约定的绑定关系,保证了接口约定文档具有百分百的可靠性。我们得以通过接口约定来构建一个可靠的测试系统,进行自动化的联调与测试。

常用的默认类型

  • Partial

把 interface 所有属性变成可选:

interface Obj {
  a: number;
  b: string;
}

type OptionalObj = Partial<Obj>

// interface OptionalObj {
//   a?: number;
//   b?: string;
// }
复制代码
  • Readonly

把 interface 所有属性变成 readonly:

interface Obj {
  a: number;
  b: string;
}

type ReadonlyObj = Readonly<Obj>

// interface ReadonlyObj {
//   readonly a: number;
//   readonly b: string;
// }
复制代码
interface T {
  a: string;
  b: number;
  c: boolean;
}

type OnlyAB = Pick<T, 'a' | 'b'>;

// interface OnlyAB {
//   a: string;
//   b: number;
// }
复制代码

在 FP 中,函数就像一个个管道,在管道的连接处的数据块的类型总是不尽相同。下一层管道使用类型往往需要重新定义。

但是如果有一个确定的推导函数返回值类型的方法,那么只需要知道管道最开始的数据块类型,那么所有管道连接处的类型都可以推导出来。

当前 TS 版本尚不支持直接获取函数返回值类型,虽然本文介绍的间接方法也能解决问题,但最好还是希望 TS 早日直接支持:issue

FP 就像一匹脱缰的野马,请用类型拴住它。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK