likes
comments
collection
share

【TypeScript】如何使用ts获取一个对象中Value指定类型的Key值组成的新类型?

作者站长头像
站长
· 阅读数 37
const SexEnum = {
    male:"男",
    female:"女"
}


// 生成对象
const SexEnumKV = {male:"male",female:"female"}

// 在指定函数中需要使用sexenum的key值时,可以得到

function getSex(sexClasses:SexEnumKV){
    return SexEnum[sexClasses]
}


getSex(SexEnumKV.male) //=> 男

这次带来的是,如何从一个对象中输出指定的新对象集合。

小王的新需求

代码员小王某天在码kpi的时候,突发奇想——如果我写一个函数,这个函数里面保存了一个对象和他的调用方法,在某个一个特定的场景下回调这个对象的调用方法,意下如何?

代码人从来不会停下手里的活,这点我就是佩服。

于是,他做了这么一个模型:


const user = {
    id:"4008",
    account:"wangtaimei",
    age:18,
    getAge(){
        return this.age
    }
}

const callwaitings = new Map<string,[object,string]>();

function mapWaitingCall(target:object,callbackName:string){
    callwaitings.set(target.id,[target,callbackName]);
}

mapWaitingCall(user,"getAge");

const [userTarget,callback] = callwaitings.get(user.id);

userTarget[callback].call(user);

如果是js程序,那么一点问题都没有—— 毕竟语法也不会报错么。但是ts程序就不同了,这不他就遇到了第一个问题:

类型“object”上不存在属性“id”。(TS2339)

function mapWaitingCall(target:object,callbackName:string){
    
    // target.id  类型“object”上不存在属性“id”。
    callwaitings.set(target.id,[target,callbackName]); 
}

这是一个什么错误呢?

object是一个基准类型,是Object类型的代称。而Object的实例对象上是不存在id这个属性的,js是基于原型链的继承关系,所有子类型可以自然向父类型转型,而父类型不可以自然向子类转型,如果需要转型就必须用强制转换:as或者 <type> 比如:

const a = [1,2,3]

const b = a as object

// 第二种写法在`java/c#`中的常见写法
const c = <object>a;

因此就自然无法获取到id了。那要如何解决?

聪明的小王已经想到了解决的办法,既然object没有,那user还能没有吗?直接把object换成user不就完了吗?

于是乎:

user”表示值,但在此处用作类型。是否指 类型 “user” ? (TS2749)

//target:user `user”表示值,但在此处用作类型。是否指` 类型 user”?
function mapWaitingCall(target:user,callbackName:string){  
    callwaitings.set(target.id,[target,callbackName]);
}

又来了一个错误。

我们在使用 const a = {} 定义一个对象数据时他没有构建对象的来源,也不是基础类型,因此采用的是对象数据的匿名形式。

除了匿名对象,常见的还有匿名函数 const fn = ()=>{} ,那数组算匿名吗?很显然,数组是一个具名的数据结构,所有数组都是Array对象的实例化,因此不算。

那如何解决?

typeof 关键字

在ts中有一个类型关键字,可以获取到一个匿名对象的原型,那就是tyoeof,比如这里的 user:

type TUser = typeof user 

function mapWaitingCall(target:TUser,callbackName:string){  
    callwaitings.set(target.id,[target,callbackName]);
}

类型“[object, string] | undefined”必须具有返回迭代器的 "[Symbol.iterator]()" 方法。ts2488

//[userTarget, callback]  类型“[object, string] | undefined”必须具有返回迭代器的 "[Symbol.iterator]()" 方法。
const [userTarget, callback] = callwaitings.get(user.id);

我们从callwatings中获取的结果是一个元组——具有固定长度、类型的数组。如何定义一个元组呢?有两种办法

// 第一种:
const a:[number,number] = [1,2]

// 我更倾向于第二种:
const b = [1,2] as const;

这里的错误是什么原因?

callwaitings.get 实际上得到的结果值为 V | undefined ,V 值得就是 [object, string] ,是可迭代的。而undefined则是不可迭代的。假如获取的key不存在的情况下,这个结果就是undefined,用自动解构就出现问题了。对于这种问题,我们有以下几点方案:

  1. 操作符 “!”

ts中有两个操作符,?和!,分别是一个值的可能不存在和一个值必然存在的断言。当我们明确知道某个结果是存在的,但是ts编译器校验无法自动识别时,可以在结果后追加!告诉编译器此时为非空断言。

 const [userTarget, callback] = callwaitings.get(user.id)!;
  1. 使用 if 缩小范围
const result = callwaitings.get(user.id);
if(result){
 const [userTarget, callback] = result;
}

于是以上的代码就变成了

const user = {
    id:"4008",
    account:"wangtaimei",
    age:18,
    getAge(){
        return this.age
    }
}
type TUser = typeof user 

const callwaitings = new Map<string,[TUser,string]>();

function mapWaitingCall(target:TUser,callbackName:string){
    callwaitings.set(target.id,[target,callbackName]);
}

mapWaitingCall(user,"getAge");

const [userTarget,callbackName] = callwaitings.get(user.id)!;

userTarget[callbackName].call(user);

虽然以上的代码不报错,但是不代表没有风险。小王同学又发现了一个问题,这里的mapWaitingCall(user,"getAge")可以随意的填入第二个参数,这就导致了,如果callbackName的值不存在于user时,语法检测是无法识别错误的,如何让写代码时,只能填入TUser类型下的值呢?

使用 keyof 关键字获取一个类型中key值合集

但是没有 valueof 关键字。为什么不设计呢?暂时想不到原因。毕竟Object中都有valueOf函数了。也许是valueof 的结果其实没什么实际意义,所以就没有设计。

这里的TUser的key值就可以用:

type TUser  = typeof user
type TUserKeySet = keyof TUser; // -> id|account|age|getAge

那我们就可以限定输入范围啦:

const callwaitings = new Map<string,[TUser,TUserKeySet]>();

function mapWaitingCall(target:TUser,callbackName:TUserKeySet){
    callwaitings.set(target.id,[target,callbackName]);
}

// 类型“"NonProp"”的参数不能赋给类型“"id" | "account" | "age" | "getAge"”的参数。
mapWaitingCall(user, "NonProp");

这样就不用担心输入时随意发挥了。不过:

类型“string | number | (() => any)”上不存在属性“call”。ts2339

//userTarget[callback].call 类型“string | number | (() => any)”上不存在属性“call”。
userTarget[callback].call(user);

小王吐血,这要怎么搞?这还能怎么搞?上面刚提的:缩小范围

const fn = userTarget[callback]

if (typeof fn == 'function') {
    fn.call(user);
}

解决是解决了,但不够优雅;

小王同学想,这里的if很明显是非必要的,我只要控制出TUserKeySet的范围都是function的key值不就可以了吗?话虽如此,现实很明显没有头绪。

小王的新的转变——类型体操

虽然ts的类型体操很是邪恶,但是不得不说,这也是他的一大特色,工作中也或多或少的离不开一些基础的体操姿势。

在练体操前,我们先讲一下体操的类型方法:

1.Partial 2.Required 3.Readonly

/**
 * 让T类型的所有属性变成可选项
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

/**
 * 让T类型所有类型变成必选项
 * 或者说 “-?” 解释为删除所有 ? 可选操作符
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

/**
 * 让t类型所有属性变成只读项
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

这三种一般只在类型拷贝时会用到。

4. Record ,构建一个由 string|number|symbol 组成的T类型集合

/**构建一个由 key为string|number|symbol value为T的新类型 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

划重点, 5.Extract 6.Exclude 7.Pick 8.Omit

/**
 *  获取 T 与 U 类型的交集
 */
type Extract<T, U> = T extends U ? T : never;

/**
 * 获取 T与U类型交集的补集,与Extract范围相对
 */
type Exclude<T, U> = T extends U ? never : T;

/**
 * 从类型T中挑选key值为K的类型勾成一个新类型
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

/**
 * 从类型T中删除K类型的新类型,与Pick相对
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

这四种操作,前两个是对集合的操作,后两个是对对象的操作。由此四个类型操作加上keyof,得到了一组完整的新对象构建方法。

as 操作符和模板字面量与三元表达式

在代码中,as代表的类型的强制转换,而在type中,代表的也是类型的重新定义。模板字面量,没想到吧,ts的type操作中同样可以拼接字符串呀。实操一下:

type getters<T>  = {
    [K in keyof T as `get${Capitalize<K & string>}`]:()=>T[K]
}

interface B  {
    name:string
}

type bGetters = getters<B> 

Capitalize 会让字符类型的第一个字母变成大写 ,Uncapitalize会让字符的第一个字母变成小写

// 于是我们就得到了
type bGetters = {  
    getName: () => string;  
}

通过这个点,我们发现了通过as,我们可以对K“重构”了!是不是又一次感叹类型体操真的太。。了?

结合如此,小王再一次奔上征程,我只要知道所有的value为function的key值就可以构建一个新的类型了!

  1. 首先,获取到所有的key值:
  2. 找到key值的value值
  3. 判断value值是否为Function,如果是则得到一个key,否则得到一个value。
  4. 构建新类型对象

于是乎:

type KeyOf<T, Type> = keyof {
    [P in keyof T as T[P] extends Type ? P : never]: T[P]
};

type TUserKeySet = KeyOf<TUser,Function>

就这样,一波云里雾里的折腾之后,小王终于得到了满意的答案。但是对于此次的操作不甚满意,毕竟他觉得,体操不够长就,any才是永恒。没有必要为了一点把戏搭进去个把小时的时间。

🌈

不说了,小王又要被KPI统计代码量了。

转载自:https://juejin.cn/post/7373588614847201290
评论
请登录