type Watcher = {
on(
eventName: string,
callback: (oldValue: any, newValue: any) => void
): void;
};
function watch(obj: object): Watcher;
function watch(obj){
function on(eventName, callback) {
}
return {
on: on,
};
};
const ageSymbol = Symbol("age");
const userWatch = watch({
name: "ray",
[ageSymbol]: 18,
job: "frontend",
experience: 7,
});
userWatch.on(
"nameChanged",
(oldValue,newValue) => {
// todo
}
)
上面代码定义了一个
watch
函数,此函数接收一个对象且有一个on
方法,on
方法接受两个参数eventName
&callback
,我们使用了type Watcher
对这个函数的使用进行了约束。
但是这样的约束无疑是毫无意义的,首先我们对于eventName
只进行了简单的string
,对于callback
的oldValue
、newValue
的类型根本就没有起到ts
的意义。下面我们将使用ts
将上面的代码改动的更规范
约束eventName
对于eventName
的约束,我们期望的类型应该是仅接受传入的对象的key
,由于是订阅,因此我们可以自定义一些解释性字符,下面我们使用${key}Changed
对于eventName
进行约束
type Watcher<T> = {
on(
eventName: `${keyof T & string}Changed`,
callback: (oldValue: any, newValue: any) => void
): void;
};
function watch<T extends object>(obj: T extends any[] ? never : T): Watcher<T>;
我们引入范型使用keyof T
获取到传入的对象的key
,由于我们仅监听为string
的key
,因此使用交叉类型将非string
的key
进行过滤,然后使用模版字符串将其与Changed
拼接。如下图所示,就得到我们所期望的eventName
T extends any[] ? never : T
:代表接收一个非数组的对象
约束callback
的参数类型
如上图所示,对于callback
的参数类型根本就没有任何判断。我们应该期望的是,对于我们传入的对象的某个key
进行订阅时,我们期望得到它的原始数据类型,当key
的值类型为number
类型时,我们在使用js原始number方法时编辑器可以智能提示,当值的类型为string
类型时,我们在使用js原始number方法时编辑器可以智能提示,等等。总之,我们期望在callback
中处理其参数使用js的原始方法更得心应手。
type Watcher<T> = {
on<K extends string & keyof T>(
eventName: `${keyof T & string}Changed`,
callback: (oldValue: T[K], newValue: T[K]) => void
): void;
};
从上面的代码可以看出,当订阅name
时,callback
的参数类型推断为string
;当订阅experience
时,callback
的参数类型推断为number
。这样显然达到了对于callback
参数的类型约束,那这样就结束了吗?我们接着往下看
对于eventName
和callback
的双重约束
我们期望的是eventName
应该为${订阅key}Changed
,从上面的图我们可以看出,我们订阅的是name
,但eventName
却为jobChanged
,这显然不是所期望的。
type Watcher<T> = {
on<K extends string & keyof T>(
eventName: `${K}Changed`,
callback: (oldValue: T[K], newValue: T[K]) => void
): void;
};
很显然,当我们获取类型断言的T[K]
时,其中K
为我们真正想要订阅的key
,因此我们直接对eventName
的约束改为K
即可,如下图所示
如上图所示,当我们输入错误的eventName
时,编辑器会自动给出错误提示。当我们输入正确的订阅key
时,编辑器也会提示出正确的eventName
。这显然是我们所期望的!而且当我们使用上面的类型约束时,其实也无须书写订阅的key
了,当我们给出正确的eventName
时,代码会自动推导出你想要订阅的key
,如下图所示:
附完整代码
type Watcher<T> = {
on<K extends string & keyof T>(
eventName: `${K}Changed`,
callback: (oldValue: T[K], newValue: T[K]) => void
): void;
};
function watch<T extends object>(obj: T extends any[] ? never : T): Watcher<T>;
function watch(obj) {
var events = {};
function on(eventName, callback) {
if (!events[eventName]) {
events[eventName] = [];
}
events[eventName].push(callback);
}
return {
on: on,
};
}