TypeScript 题
在读深入理解typescript读到infer这张遇到的这个题,最后参考当前文章
我们看完这些,我们发现typescript可以减少90%的一些拼写、字段漏写、传参不匹配等基本错误。还可以增减代码的健壮性,代码的可维护性提升。
这个题目是一个很有代表意义的题,在实际工作中也会遇到类似的问题。
问题定义
假设有一个叫EffectModule
的类
假设有一个叫 EffectModule 的类
这个对象上的方法只可能有两种类型签名:
interface Action<T> {
payload?: T
type: string
}
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>
syncMethod<T, U>(action: Action<T>): Action<U>
这个对象上还可能有一些任意的非函数属性:
interface Action<T> {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise<number>) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}));
}
setMessage(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}
现在有一个叫connect
的函数,它接受EffectModule
实例,将它变成另一个对象,这个对象上只有EffectModule
的同名方法,但是方法的类型签名被改变了:
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> 变成了
asyncMethod<T, U>(input: T): Action<U>
syncMethod<T, U>(action: Action<T>): Action<U> 变成了
syncMethod<T, U>(action: T): Action<U>
例子:
EffectModule 定义如下:
interface Action<T> {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise<number>) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}));
}
setMessage(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}
connect 之后:
type Connected = {
delay(input: number): Action<string>
setMessage(action: Date): Action<number>
}
const effectModule = new EffectModule()
const connected: Connected = connect(effectModule)
要求
在 题目链接 里面的 index.ts
文件中,有一个 type Connect = (module: EffectModule) => any
,将 any
替换成题目的解答,让编译能够顺利通过,并且 index.ts
中 connected
的类型与:
type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
}
完全匹配。
解答
- 挑选函数
- 取待推断的变量类型
补充
type FuncName<T> = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T];
详细
题目要求是把 type Connect 中的 any 替换掉,并符合:
type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
}
并编译通过。
再把问题简化一下,就是设计一个工具类型,让题目中的EffectModule
的实例转化为符合要求的Connected
。
即:
type Connect = (module: EffectModule) => xxx ---> type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
}
仔细观察上面的伪代码实例,Connected
其实是一个对象类型,其中包含的 key-value
就是EffectModule
中的方法转化而来的,所以我们的入手处就是想办法将EffectModule
中的方法转化为对应的Connected
中的 key-value
。
type Connect = (module: EffectModule) => {
...
}
再观察 Connected 的属性与 EffectModule 的方法是不是有共同之处?他们的名字是一样的,所以我们得先设计一个工具类型把 EffectModule 中的方法名取出来。
但EffectModule
,包含非方法的「属性」,所以得做个判断,如果是属性值类型是函数那么取出,否则不要取出
type FuncName<T> = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T];
方法的类型签名被改变了:
asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>> 变成了
asyncMethodConnect<T, U> = (input: T) => Action<U>
syncMethod<T, U> = (action: Action<T>) => Action<U> 变成了
syncMethodConnect<T, U> = (action: T) => Action<U>
着手转化工作
type EffectModuleMethodsConnect<T> = T extends asyncMethod<infer U, infer V>
? asyncMethodConnect<U, V>
: T extends syncMethod<infer U, infer V>
? syncMethodConnect<U, V>
: never;
目前我们有两个主要的工具类型EffectModuleMethodsConnect
负责类型的转化,methodsPick
负责取出方法名,现在我们先把方法名取出:
type EffectModuleMethods = FuncName<EffectModule>
最后
type Connect = (module: EffectModule) => {
[M in EffectModuleMethods]: EffectModuleMethodsConnect<EffectModule[M]>
}
// import { expect } from "chai";
interface Action<T> {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise<number>) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}));
}
setMessage(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}
type MethodPick<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>>;
type asyncMethodConnect<T, U> = (input: T) => Action<U>;
type syncMethod<T, U> = (input: Action<T>) => Action<U>;
type syncMethodConnect<T, U> = (input: T) => Action<U>;
type EffectModuleMethodsConnect<T> = T extends asyncMethod<infer U, infer V>
? asyncMethodConnect<U, V>
: T extends syncMethod<infer U, infer V>
? syncMethodConnect<U, V>
: never;
type EffectModuleMethods = MethodPick<EffectModule>;
// 修改 Connect 的类型,让 connected 的类型变成预期的类型
type Connect = (module: EffectModule) => {
[M in EffectModuleMethods]: EffectModuleMethodsConnect<EffectModule[M]>
};
const connect: Connect = m => ({
delay: (input: number) => ({
type: 'delay',
payload: `hello 2`
}),
setMessage: (input: Date) => ({
type: "set-message",
payload: input.getMilliseconds()
})
});
type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
};
export const connected: Connected = connect(new EffectModule());