TypeScript 下 Mapped Types 应用场景例
技术TypeScript, DX

TypeScript 下 Mapped Types 应用场景例

Published On
| 更新于
Updated On
6分钟阅读概述:作者阐述了,开发者可以通过 TypeScript 的 Mapped Types 特性,在具体场景下,来有效减负对字段 field 匹配的 setField/toggleField 的 TS 接口声明和具体的函数定义工作。

最前

必要外链

权威章节阅读,来自 TS 官方手册下章节:

直观效果

const initState: InitState = {
  name: "nimo", // 🐟
  id: 42,
  isMan: false,
  isFish: true,
};

const actions = getTargetActionsUponState<PartialState, PartialBoolState>(
  initState,
  set,
);

console.log(actions);
/* 👉🏻 控制台输出如下:
[Object: null prototype] {
  setName: [Function (anonymous)],
  setId: [Function (anonymous)],
  toggleIsMan: [Function (anonymous)],
  toggleIsFish: [Function (anonymous)]
}
*/

背景

在如 redux (redux-toolkit) 或者 zustand 为代表的将全局状态中心化管理的库,通常离不开对初始状态 initialState 和相关动作函数 #action 的声明。大致形如:

const initState = {
  foo: 42,
  bar: false,
};

function setFoo(val) {
  return {
    type: type4SetFoo /* may not necessary*/,
    payload: {
      foo: val,
    },
  };
}

function setBar(val) {
  return {
    type: type4SetBar /* may not necessary*/,
    payload: {
      bar: val,
    },
  };
}

而实际业务中,初始状态下的字段数量通常是较为庞大的,而基本这些状态值当中,相当一部分,我们是需要为其设置独立的 #set 或者 #reset 函数。

所以未作特别处理情况下:初始状态字段越多,需要开发者额外定义的 #set 函数也越多

当然,编辑器中支持简单的 code snippet 定义,做好相关设置,根据 #bar 字段,自动生成 #setBar 函数也是能够做到的。但是如此代码中,实际仍然是不可避免的显式出现多个 #setSomeField 的定义。

所以下面,我们尝试依赖 TypeScript 的Mapped Types 特性支持,来有效减负这方面的工作。能够做到一次声明,为后续节省掉:

  1. #setField 的 TS 定义;
  2. #setField 的具体实现的函数声明;

这么两项工作任务。

正题

TypeScript 利器

首先来看一段官方示例代码:

type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<
    string & Property
  >}`]: () => Type[Property];
};

interface Person {
  name: string;
  age: number;
  location: string;
}

type LazyPerson = Getters<Person>;

// equal to:
/* type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
} */

而我们可以参考这段代码作如下定义:

type Action4State<T> = {
  readonly [P in keyof T as `set${Capitalize<string & P>}`]: (
    value: T[P],
  ) => void;
};

获得联想支持,如下处理:

type State = {
  foo: number;
  bar: boolean;
};

const actionsInvalid = {};

// 🎈 现在我们获得了约定的形如 `setField` 的联想支持,如下的:
(actionsInvalid as unknown as Action4State<State>).setBar;
(actionsInvalid as unknown as Action4State<State>).setFoo;

类型推断具备后,还需要实际功能函数的定义

类型推断具备,还需要实际功能函数的定义。我们需要根据处理的 initState, 生成目标 action 函数,处理风格并不唯一,下面分享吾辈的处理:

// 🎈 这里假定接收的 `#set` 入参就是后续调用时,由状态管理库提供的 `#set` 或近似函数
export function getTargetActionsUponState<T>(
  initState: T,
  set: (...args: unknown[]) => void,
): Action4State<T> {
  return Object.keys(initState as object).reduce((acc, key) => {
    const funcName = `set${getCapitalize(key)}`;
    acc[funcName] = (value: any) => set({ [key]: value });
    return acc;
  }, Object.create(null));
}

function getCapitalize(key: string) {
  if (!key) {
    throw new Error("❌ invalid param");
  }
  return key[0].toUpperCase() + key.slice(1);
}

如下的应用:

/* === TEST === */
const initState: State = {
  foo: 1,
  bar: false,
};

// 必要的 #set 函数
function set(val: unknown) {
  /* ... */
}

const actions = getTargetActionsUponState<State>(initState, set);

console.log(actions);
/* 👉🏻 控制台输出如下:
  [Object: null prototype] {
    setFoo: [Function (anonymous)],
    setBar: [Function (anonymous)]
  }
*/

延伸

更友好的 API 暴露风格

可以从个人开发喜好上出发,比如对于 initState 下的布尔值字段,相比于其对应的 action 函数命名为 #setBar,更偏好于命名为 #toggleBar

如此的,可以进一步拓展上面的 TypeScript 接口定义,同时完善 #getTargetActionsUponState 函数。

但目前吾辈实现也较为粗糙,实现如下:

code block

THX

感谢该开发者的博客分享:试了一下 Zustand ,一个挺简洁的状态管理库

TypeScript 下 Mapped Types 应用场景例

https://blog.ninoh.cc/blog/a4-ts-mapped-types[Copy]
本文作者
ninohx96
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。