TypeScript 下 Mapped Types 应用场景例
- Published On
- Updated On
最前
必要外链
权威章节阅读,来自 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 特性支持,来有效减负这方面的工作。能够做到一次声明,为后续节省掉:
- 对
#setField
的 TS 定义; - 对
#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
函数。
但目前吾辈实现也较为粗糙,实现如下:
THX
感谢该开发者的博客分享:试了一下 Zustand ,一个挺简洁的状态管理库
TypeScript 下 Mapped Types 应用场景例
https://blog.ninoh.cc/blog/a4-ts-mapped-types[Copy]转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。