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

简介: ![](http://p1.ifengimg.com/a/2016_19/ea0b92b08ab62d2_size47_w550_h275.jpg) 自从 `Redux` 诞生后,函数式编程在前端一直很热;去年7月,`Typescript` 发布 2.0,OOP 数据流框架也开始火热,社区更倾向于类型友好、没有 Redux 那么冗长烦琐的 Mobx 和 [dob](https://githu

自从 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 供读者体验:

https://github.com/jasonHzq/redux-ts-helper

读者可以 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;
// }
  • Pick
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 就像一匹脱缰的野马,请用类型拴住它。

相关文章
|
2月前
|
JavaScript
typeScript进阶(9)_type类型别名
本文介绍了TypeScript中类型别名的概念和用法。类型别名使用`type`关键字定义,可以为现有类型起一个新的名字,使代码更加清晰易懂。文章通过具体示例展示了如何定义类型别名以及如何在函数中使用类型别名。
39 1
typeScript进阶(9)_type类型别名
|
2月前
|
JavaScript
typeScript基础(2)_any任意值类型和类型推论
本文介绍了TypeScript中的`any`任意值类型,它可以赋值为其他任何类型。同时,文章还解释了TypeScript中的类型推论机制,即在没有明确指定类型时,TypeScript如何根据变量的初始赋值来推断其类型。如果变量初始化时未指定类型,将被推断为`any`类型,从而允许赋予任何类型的值。
55 4
|
28天前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
44 0
|
28天前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧
|
2月前
|
JavaScript
typeScript基础(5)_对象的类型-interfaces接口
本文介绍了TypeScript中接口(interfaces)的基本概念和用法,包括如何定义接口、接口的简单使用、自定义属性、以及如何使用`readonly`关键字定义只读属性。接口在TypeScript中是定义对象形状的重要方式,可以规定对象的必有属性、可选属性、自定义属性和只读属性。
39 1
|
2月前
|
存储 JavaScript
typeScript进阶(11)_元组类型
本文介绍了TypeScript中的元组(Tuple)类型,它是一种特殊的数组类型,可以存储不同类型的元素。文章通过示例展示了如何声明元组类型以及如何给元组赋值。元组类型在定义时需要指定数组中每一项的类型,且在赋值时必须满足这些类型约束。此外,还探讨了如何给元组类型添加额外的元素,这些元素必须符合元组类型中定义的类型联合。
44 0
|
2月前
|
JavaScript
typeScript进阶(10)_字符串字面量类型
本文介绍了TypeScript中的字符串字面量类型,这种类型用来限制变量只能是某些特定的字符串字面量。通过使用`type`关键字声明,可以确保变量的值限定在预定义的字符串字面量集合中。文章通过示例代码展示了如何声明和使用字符串字面量类型,并说明了它在函数默认参数中的应用。
36 0
|
11天前
|
JavaScript 开发者
在 Babel 插件中使用 TypeScript 类型
【10月更文挑战第23天】可以在 Babel 插件中更有效地使用 TypeScript 类型,提高插件的开发效率和质量,减少潜在的类型错误。同时,也有助于提升代码的可理解性和可维护性,使插件的功能更易于扩展和升级。
|
23天前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
25天前
|
JavaScript 前端开发 安全
TypeScript【基础类型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第9天】TypeScript【基础类型】超简洁教程!再也不用看臭又长的TypeScript文档了!
下一篇
无影云桌面