【TypeScript】如何使用ts获取一个对象中Value指定类型的Key值组成的新类型?
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,用自动解构就出现问题了。对于这种问题,我们有以下几点方案:
- 操作符 “!”
ts中有两个操作符,?和!,分别是一个值的可能不存在和一个值必然存在的断言。当我们明确知道某个结果是存在的,但是ts编译器校验无法自动识别时,可以在结果后追加!告诉编译器此时为非空断言。
const [userTarget, callback] = callwaitings.get(user.id)!;
- 使用 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值就可以构建一个新的类型了!
- 首先,获取到所有的key值:
- 找到key值的value值
- 判断value值是否为Function,如果是则得到一个key,否则得到一个value。
- 构建新类型对象
于是乎:
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