【typescript】如何从对象中生成一个key-value一致的新对象,keyof typeof的使用

327 阅读3分钟

引言

如何对一个对象操作并且可以得到期望值一直是新手的一个痛点,今天我就来说一说这个事情。

最近在开发一个菜单插件,需要定义一组菜单的操作按钮,其中有一个需要展示菜单事件定义的操作。

Object.keys 的操作

对象的操作一般都很简单,无非就是取值还是赋值,以及一些key、value的操作。但是融入了ts之后,这些操作就变得复杂起来。首先,ts中Object的静态方法比如Object.keys() 得到的结果集是string[],并支持得到一个对象的的key序列,对于很多人来说就比较头痛了。

const person = {name:"John",age:13,sex:"M"}

const keys = Object.keys(person);

// 得到
const keys: string[]

// 期望
type keys = keyof typeof person;
type keys = "name" | "age" | "sex"

如果需要以上的type-keys,则需要

const keys = Object.keys(person) as unknown as (keyof typeof person)[];

//得到
const keys: ("name" | "age" | "sex")[]

语法句子不是很复杂,我们逐句解析一下。

  1. Object.keys(person)

    • 得到 string[] 的结果集
  2. as unknown

    • 将上一个类型转化为未知类型
    • as 是类型断言,断言的意思是:我认为它必然是 as 后面这种类型的。但是只能是以any或者unknown的类型或者向父类转型,否则不可用
      • 类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。
    • 有一些小伙伴在开发时喜欢用any,就好比html全部使用div+span一样,这种做法不是很友好
  3. keyof typeof person

    • typeof person 获取一个实例的原始类型。
    • keyof 获取一个类型的key值
    • (keyof typeof person)[] 获得person类型的key值集合

正文

有了以上的基础经验,那我们就来设计右键菜单的定义吧。

菜单单项类型定义

// 单项
interface ContextItem<E> {
    // 展示文本
    label:string;
    // 判定条件
    condition?(args:any):boolean
    
    // 回调事件
    event:E;
    // 传入参数列表
    props?:string[]
    
    // 分割线
    split?:boolean
}

这里使用了一个泛型类型E,通常我们对泛型定义时,使用简短缩写字母,避免与现有的类型产生名称冲突

定义菜单的其他相关操作

菜单定义的模板文件

// template.menu.define.ts

/*菜单id*/
const id = ''

/*操作事件*/
const events = {} as const

/*展示项目*/
const items:ContextItem<keyof typeof events>[] = [];

到这里其实就差不多结束了。

使用以上模板构建一个用户的菜单配置项。

// person.menu.define.ts

/*菜单id*/
const id = 'Person'

/*操作事件*/
const events = {
    setAge(person:Person,age:number){
        person.age = age;
    }
} as const

/*展示项目*/
const items:ContextItem<keyof typeof events>[] = [
    {
        label:"设置年龄",
        // 定义event时,会从events的key里面提示对应的名称
        event:'setAge', 
        props:['person','age'],
        condition({age}){
            return age == 0;
        }
    }
];

使用时,通过items生成菜单选项,点击后获取到被点击的ContextItem项目,使用 event 从events里获取事件,并且传递参数给到操作按钮

function menuClick(item:ContextItem<string>,argsProvider:()=>Record<string,any>){
    
    const {
        props,
        event:eventName,
    } = item
    
    const callback = events[event];
    
    const args = argsProvider();
    
    const arguments = props.map(prop=>args[prop]);
    
    callback.apply(null,arguments);
}

// 使用步骤

const person = {name:"John",age:13,sex:"M"}

// 提供给event的参数内容
const argsProvider = ()=> ({ person, age:11 })

// 被选中的单项
const item = items[0] 

menuClick(item,argsProvider);

衍生需求,从一个菜单调用另一个菜单的操作

【从一个对象生成一个KV等值的新对象】 。

当菜单按钮需要对第三人提供操作时,为了代码友好性,则需要添加一层指令操作,让调用方可以快速获悉菜单的操作方式和方法。那么,则有以下ts的代码提示项

// 提供
const events = {
    setAge(person:Person,age:number){
        person.age = age;
    }
} as const

// 期望
const eventSKV = { setAge: "nsetAgeame"}

// 操作过程
type EventKeys = keyof typeof eventSKV;
const eskv = Object
    .keys(events)
    .reduce((types, v: any) => (types[v] = v, types), {} as any) as
    { [key in EventKeys]: key }
    
// 得到
const eskv: {  
    setAge: "setAge";  
}

如此使用eskv时,就可以得到setAge等等的提示了。

补充

  1. 如何获取一个函数的参数列表?
    • Parameters<typeof functionName>
  2. 如何获取一个函数的返回值类型?
    • ReturnType<typeof functionName>